Text
                    Pocket PC
Руководство разработчика
Создание приложений
для мобильных устройств
на платформе Microsoft -
от Windows СЕ до
Mobile
Методы проектирования
ди 'Фирменно-
k-jgaa*CHMbix программ
Снтимйзация и отладка
приложений для Pocket PC
Написание программ
учетом малого размера
^экрана Pocket PC
БРЮС Е. КРЕЛЛЬ

УДК 004.4 ББК 32.973.26-018.2 К79 Брюс Е. Крелль К79 Pocket PC. Руководство разработчика. - М.: Издательский дом ДМК-Пресс, 2007. - 352 с.: ил. ISBN 5-9706-0031-8 http: //all - ebooks. com Из этой книги вы узнаете, как можно создавать эффективные программы для КПК (карманных персональных компьютеров) на базе операционных систем Windows СЕ и Windows Mobile. Вы найдете здесь библиотеки и инструменты, кото- рые помогут заметно сократить время разработки проектов. На примере работаю- щих программ продемонстрирована техника построения графических интерфейсов на маленьком экране Pocket PC. В издании рассмотрена архитектура Windows СЕ, работа с СОМ-объектами, проектирование многопоточных приложений и синхронизация, а также оптимиза- ция и отладка программ и их компонентов. УДК 004.4 ББК 32.973.26-018.2 Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разреше- ния владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответст- венности за возможные ошибки, связанные с использованием книга. ISBN 5-9706-0031-8 © Брюс Е. Крелль, 2005 © Оформление, Издательский дом ДМК-пресс, 2007
Содержание Благодарности..........................................12 Предисловие............................................13 К обязательному прочтению! ..........................13 На кого рассчитана эта книга?........................13 Каковы особенности этой книги?.......................13 Что необходимо для чтения этой книги?................14 Какова роль примечаний?..............................14 На какой платформе тестировались программы?..........14 Что можно сказать о включенных в книгу программах?...15 Как связаться с автором?.............................15 Глава 1. Обзор платформы Pocket PC.....................16 Основные элементы интерфейса пользователя............17 Архитектура Windows СЕ...............................18 Внутри подсистемы GWE................................22 Обзор интерфейса графических устройств (GDI).........24 Логическая структура программы для Windows...........28 Обработка сообщений в программе....................29 Обновление клиентской области окна.................. 30 Резюме...............................................31 Глава 2. Типичная программа для Pocket PC..............32 Уникальные особенности программ для Windows СЕ.......32 Тип TCHAR - основа переносимого механизма обработки строк......................................35 Анатомия простой программы для Windows...............36 Общая логическая структура программ для Windows......40 Типичная программа для Windows.......................40 Полный текст функции WinMain.......................41 Построчный анализ функции WinMain .................42 Полный текст функции WinProc ......................47
1111 Содержание Построчный анализ функции WinProc.................48 Преобразование программы для исполнения на платформе Windows СЕ.............................53 Модификации функции WinMain.......................53 Обсуждение модификаций WinMain....................54 Аннотированный исходный текст модифицированной функции WinMain...................................54 Модификация функции WinProc.......................56 Обсуждение модификаций WinProc....................56 Аннотированный исходный текст модифицированной функции WinProc...................................57 Анализ проекта простой программы для Windows........58 Резюме..............................................61 Примеры программ в Web............................ 61 Инструкции по сборке и запуску....................62 Глава 3. Минимальная легко тестируемая программа для Pocket PC.........................................63 Пользовательский интерфейс минимальной программы для Pocket PC.......................................63 Проектирование минимальной программы для Pocket PC...64 Анализаторы сообщений...............................67 Работа с мастером Message Cracker Wizard............70 Реализация минимального диалога.....................73 Шаблоны диалогов и меню...........................74 Функция WinMain...................................75 Функция Dig Proc..................................77 Тела обработчиков сообщений.......................79 Компонент PortabilityUtils........................81 Компонент DataMgr.................................84 Сборка программы для настольного ПК.................85 Перенос программы на КПК............................85 Анализ проекта минимальной диалоговой программы.....87 Резюме..............................................88 Примеры программ в Web..............................88 Инструкции по сборке и запуску....................88 Глава 4. Обзор платформы Pocket PC....................90 Графический интерфейс пользователя для простой программы анимации......................90 Рисование изображений............................. 91
Содержание 11И1ПИ Использование набора инструментов рисования.......92 Имеющиеся стили пера и кисти......................93 Операции рисования................................95 Операции отсечения................................96 Вывод изображения.................................98 Принудительная перерисовка окна приложения........99 Использование таймеров...........................100 Применение инкапсуляции в проекте приложения.......102 Реализация простой анимационной программы..........105 Анализ функции DlgProc...........................106 Анализ эффективности инкапсуляции..................114 Резюме.............................................114 Примеры программ в Web.............................115 Инструкции по сборке и запуску...................115 Глава 5. Реализация программы рисования..............117 Рисование объектов с помощью эластичного контура...118 Добавление объявлений и тел обработчиков сообщений .... 122 Объявление статических переменных для поддержки буксировки ........................123 Реализация рисования в обработчиках сообщений....124 Модификация обработчика WM_PAINT для поддержки стирания и рисования.............................126 Ввод и эхо-вывод символов..........................127 Реализация функций, инкапсулирующих работу с текстом .... 133 Добавление переменных для хранения состояния и текстовой строки...............................135 Обработчик сообщения WM_POSITIONCARET............136 Добавление обработки сообщений о введенных символах ... 137 Реализация обработчика сообщения WM_KEYDOWN......138 Модификация обработчика сообщений WM_LBLITTON DOWN.................................140 Реализация обработчика сообщения WM_CHAR.........142 Реализация обработчика сообщения WM_POSITIONCARET . 143 Отображение строки в обработчике сообщения WM_PAINT . 144 Критика подхода к проектированию и реализации....145 Резюме.............................................145 Примеры программ в Web.............................146 Глава 6. Обработка растровых изображений.............149 Реализация программы обработки изображений ........150
МИН Содержание Описание пользовательского интерфейса программы....150 Анализ организации программы......................155 Реализация программы обработки изображений........160 Разработка заставки с помощью функций из файла Bitmaputilities............................171 Описание пользовательского интерфейса программы....171 Описание внутренней работы программы .............172 Реализация программы вывода заставки..............173 Анимация изображения................................176 Описание пользовательского интерфейса программы....177 Реализация программы анимации изображения.........178 Подготовка A^tiveSync для программ из этой главы....184 Резюме..............................................187 Примеры программ в Web..............................187 Инструкции по сборке и запуску....................188 Глава 7. Проектирование эффективных программ..............................................192 Обоснование выбранного подхода к проектированию......193 Окончательное разбиение на уровни...................205 Процесс реализации..................................207 Анализ кода.........................................208 Реализация менеджера типов данных DrawObjMgr......208 Реализация менеджера объектов DefaultMgr..........211 Добавление переменных и методов доступа в компонент DataMgr...............................212 Добавление компонента CaretMgr....................213 Реализация компонента UserlnputMgr для обработки сообщений...........................214 Модификация обработчиков в DlgProc для взаимодействия с UserlnputMgr.................219 Расширение главного меню..........................220 Модификация обработчика сообщения WM_COMMAND с учетом пунктов меню.............................225 Добавление обработчика WMJNITMENUPOPUP для индикации выбранной фигуры....................226 Замечания по поводу проекта и реализации............227 Резюме..............................................227 Примеры программ в Web..............................228 Инструкции по сборке и запуску....................228
Содержание lllllll 9 Глава 8. Применение встроенных элементов управления в графическом интерфейсе пользователя......230 Применение встроенных элементов управления в приложении........................................230 Обзор встроенных элементов управления.............232 Реализация интерфейса со встроенными элементами управления........................................237 К вопросу о переносимости........................239 Использование групп элементов управления для реализации дружелюбного интерфейса..............240 Применение полосы прокрутки в паре с полем ввода..241 Включение дружелюбной полосы прокрутки............244 Контроль прямого ввода в парное поле .............248 Резюме..............................................249 Примеры программ в Web..............................249 Инструкции по сборке и запуску....................250 Глава 9. Разработка сложного интерфейса пользователя..........................................252 Программа рисования со сложным интерфейсом пользователя........................................252 Применение графических кнопок для организации иерархий............................................257 Шаги, необхимые для включения в программу графических кнопок................................257 Пример добавления графических кнопок..............258 Обзор реализации BitmapButtonMgr..................262 Применение вкладок для организации категорий........263 Шаги, необходимые для работы с компонентом TabPageMgr и шаблонами вкладок....................264 Пример включения компонента TabPageMgr...........264 Обзор реализации шаблонов страниц со вкладками....270 Заключительные замечания для разработчиков..........271 Резюме..............................................272 Примеры программ в Web..............................272 Инструкции по сборке и запуску....................272 Глава 10. Сохранение параметро в приложения...........274 Применение идеи многоуровневого дизайна к решению задачи о хранении параметров .............275
Illi Содержание Выбор формата хранения.............................278 Настройка менеджера базы данных параметров.........279 Пример настройки менеджера базы данных параметров...280 Определение структуры записи в базе данных параметров.......................................280 Определение записей по умолчанию для каждого параметра........................................281 Использование функций для взаимодействия с базой данных параметров........................281 Обзор реализации уровней...........................283 Конфигурирование нижнего уровня для конкретного хранилища..........................................285 Резюме.............................................285 Примеры программ в Web.............................286 Инструкции по сборке и запуску...................286 Глава 11. Многопоточные приложения и синхронизация......................................288 Разумное и неразумное применение потоков...........288 Состояния потока.................................290 Планирование потоков.............................291 Управление приоритетами..........................292 Демонстрация влияния приоритетов.................293 Введение в проблему синхронизации..................295 Решение проблемы синхронизации...................298 Некоторые детали проектирования..................299 Реализация синхронизованных потоков................301 Создание потоков.................................301 Реализация потока WinMain........................302 Реализация дочернего потока........................302 Создание объектов синхронизации..................303 Ожидание завершения шага.........................303 Отправка сигнала о завершении шага...............304 Ожидание завершения дочерних потоков.............304 Резюме.............................................305 Примеры программ в Web.............................305 Инструкции по сборке и запуску...................306 Глава 12. Использование СОМ-объектов.................308 Модель компонентных объектов.......................308 Создание СОМ-объектов с помощью библиотеки ATL. -318
Содержание и Создание COM-объекта с помощью мастера ATL СОМ AppWizard........................................319 Вставка нового объекта с помощью мастера ATL Object Wizard................................320 Добавление методов объекта с помощью мастера Add Method to Interface Wizard...................324 Реализация методов объекта.......................325 Анализ COM-объекта, созданного с помощью ATL.......326 Объявление класса................................326 Определение класса...............................328 Глобальные функции и объекты.....................329 Файл описания интерфейса.........................330 Сценарий реестра.................................331 Создание СОМ-клиента...............................332 Получение информации об интерфейсе СОМ-объекта...333 Программирование доступа к СОМ-объекту через интерфейс..................................334 Уничтожение объекта..............................335 Регистрация СОМ-сервера на Pocket PC...............336 Резюме.............................................337 Примеры программ в Web.............................337 Инструкции по сборке и запуску...................338 Предметный указатель.................................341 http: //all - ebooks. com
Благодарности Ha этапе вынашивания замысла книги принимают участие множество людей. Они немало способствуют ее созреванию, причем часто их роль остается неизвестной стороннему наблюдателю. И эта книга не исключение. Поэтому я выражаю благо- дарность всем тем, кто внес свой вклад: □ Мичико Крелль, мой жене, которая воодушевляет меня на решение нераз- решимых на первый взгляд задач; □ Вэнди Ринальди, главному редактору издательства McGraw-Hill / Osborne за ее бесконечное терпение, с которым она относилась к забракованным впоследствии черновым вариантам глав; □ Тиму Мадриду, отвечающему в McGraw-Hill / Osborne за заключение до- говоров, который прилагал все усилия, чтобы у меня было все необходимое для работы над рукописью; □ Джэнет Уолден, выпускающему редактору McGraw-Hill / Osborne, за ком- петентное и скрупулезное руководство процедурой редактирования; □ Барту Риду, литературному редактору McGraw-Hill / Osborne, за внима- тельное прочтение и замечания к тексту черновых глав; □ Кену Миллеру, президенту компании 32Х Corporation, за техническое ре- цензирование и многочисленные замечания, позволившие улучшить ка- чество рукописи; □ . Майку Мельцеру, сотруднику Pocket PC Group в Microsoft Corporation, за то, что он познакомил меня с нужными людьми, в результате чего я и полу- чил предложение написать эту книгу; □ Норму Чэндлеру-старшему и Норму Чэндлеру-младшему, которые по- буждали меня узнать о полиции и «морских снайперах» достаточно для того, чтобы разработать программу для Pocket PC имеющую коммерче- скую ценность; □ старшему сержанту морской пехоты США Биллу Скайлзу за предостав- ленную возможность на опыте понять, в чем нуждаются морские снайперы во время проведения операций; □ и не в последнюю очередь хотел бы поблагодарить всех сотрудников изда- тельства McGraw-Hill / Osborne, которые, оставаясь незаметными, выпол- няли сотни дел, без которых книга не вышла бы в свет.
http: //all - ebooks. com Предисловие К обязательному прочтению! Наверное, предисловие - это та часть книги, которую читают реже всего. А ведь в нем немало полезной информации. Потому я так и назвал этот раздел - чтобы привлечь твое внимание, читатель, в надежде, что ты все-таки не пройдешь мимо. На кого рассчитана эта книга? Книга рассчитана на любого программиста, который хотел бы писать про- граммы для Pocket PC. Кроме того, у читателя должен быть интерес к методам проектирования эффективных программ, а также к инструментам и способам раз- работки программного обеспечения в срок и не выходя за пределы бюджета. Каковы особенности этой книги? Эта книга во многом отличается от других книг на тему программирования для Pocket PC. Перечислю некоторые отличительные черты: □ упор на принципы качественного проектирования программ в сочетании с использованием библиотек; □ особенности проектирования интерфейса пользователя на экране малого размера; □ предоставление библиотек и инструментов для быстрой реализации слож- ных задач; □ перечисление шагов, необходимых для эффективного включения библио- тек в программу; □ демонстрация каждого шага на примерах, сопровождаемых подробными пояснениями; □ разработка среды для отладки приложения на настольном компьютере с последующим переносом на Pocket PC путем модификации единствен- ного флага в программе. Каждая строка включенного в книгу кода была использована в одной из рабо- тающих коммерческих программ, написанных автором. Этот код в течение не- скольких лет разрабатывался и тестировался для настольных ПК, а затем был пе- ренесен на Pocket PC. В дополнение ко всему вышеупомянутому книга содержит сотни советов, ограничений и обходных путей, к которым приходится прибегать при програм-
Hill Предисловие мировании для Pocket PC. Это результат тысяч часов, проведенных за отладкой программ. Вооружившись инструментами, приемами и знаниями, почерпнутыми из этой книги, вы сможете создавать коммерческие программы для Pocket PC в срок и не выходя из сметы. Что необходимо для чтения этой книги? Почти все представленные в этой книге программы написаны на языке С. Примеры из главы 12, посвященной модели компонентных объектов, написаны на C++. Но, чтобы понять их, не нужно быть экспертом в C++. Использование специфических для C++ возможностей сведено к минимуму, чтобы это не мешало восприятию кода. Хотя и существует версия Visual Basic для Pocket PC, но по степени зрелости она сильно уступает языку С, поэтому Visual Basic в этой книге не используется. Чтобы облегчить решение типичных задач, Visual Basic скрывает от программис- та многие детали, тем самым существенно ограничивая его возможности. Предполагается также, что вы знакомы с интегрированной средой разработки Embedded Visual Studio 3.0. Если вам доводилось работать с Visual Studio 6.0, то проблем с освоением Embedded Visual Studio 3.0 не возникнет. В этой книге вы не найдете материалов по Embedded Visual Studio 3.0, разве что в тех местах, где нуж- но продемонстрировать конкретные возможности, относящиеся к рассматривае- мой программе. Какова роль примечаний? По всему тексту разбросаны примечания примерно такого вида: ПРИМЕЧАНИЕ Примечание содержит особо важный материал, заслуживающий пристального внимания. Как правило, в примечания я помещаю те мысли, которые вы должны понять или запомнить. Помимо примечаний, вам встретятся вставки «к сведению» и «пре- достережение». На какой платформе тестировались программы? Все примеры и библиотеки, включенные в эту книгу, были протестированы на настольном ПК с Visual Studio 6.0 и на КПК Casio Cassiopeia ЕМ-500 (под кодо- вым названием «The Grape»). При тестировании на платформе Pocket PC приме- нялась Embedded Visual Studio 3.0 с библиотекой Pocket PC 2002 SDK.
Предисловие 15 ПРИМЕЧАНИЕ Хотя все программы работали на КПК, применявшемся для тестирования, не ис- ключено, что на вашем КПК они работать не будут. Каждый производитель обо- рудования пользуется специальной программой Platform Builder для настройки операционной системы Windows СЕ. Частью этой процедуры служит настройка комплекта SDK для разработки приложений. Во время тестирования программ для этой книги я то и дело натыкался на функции из SDK, которые должны были бы работать на Pocket PC, но даже не компоновались. Так получилось потому, что фирма-производитель Casio сочла необходимым удалить эти функции из SDK в процессе настройки ОС на свою платформу. При разработке библиотек и примеров я старался ограничиваться лишь теми средствами, которые, скорее всего, доступны на любой платформе. И все же что- то может на вашем КПК и не работать. Что можно сказать о включенных в книгу программах? В конце каждой главы приводится полная инструкция по сборке, установке и выполнению разработанных в ней программ с перечислением всех требований к платформе. Я старался ничего не упустить. Все инструкции были тщательны протестированы на реальной машине. Программы, которые вы можете загрузить с сайта этой книги по адресу http:// www.osborne.com, организованы в виде нескольких папок. Имя каждой папки от- ражает назначение программы. На мой взгляд, это удобнее традиционной схемы, согласно которой имена папок соответствуют номерам глав и разделов. ПРИМЕЧАНИЕ Хотя были приложены все усилия к тому, чтобы выверить текстовый и иллюстра- тивный материал, но в книге такого размеры опечатки неизбежны. Вся ответст- венность ложится на автора, поэтому я заранее приношу извинения за ошибки, которые остались незамеченными. Как связаться с автором? Связаться со мной можно по электронной почте BKrell@SWA-Engineering.com. Я буду рад ответить на технические вопросы в меру своих возможностей.
http: //all - ebooks. com Глава 1. Обзор платформы Pocket PC В основе любой версии Pocket PC (РРС) лежит операционная система Windows СЕ. Она отвечает за отображение окон, обслуживание «событий мыши», возникаю- щих при касании экрана кончиком стилоса, и обновление экрана. Но для коорди- нации работы различных элементов Windows СЕ нужна программа. В этой главе мы опишем составные части Windows СЕ и то, как написанное вами приложение взаимодействует с ними. ПРИМЕЧАНИЕ VWrafcws СЕ - это самая недооцененная на сегодняшний день операционная си- стема. Операционная система Windows СЕ обладает целым рядом важных и весьма впечатляющих особенностей. Это не что иное, как сокращенная версия ОС Windows 2000! Основные элементы Windows 2000 присутствуют и в Windows СЕ. Более того, приложение для Pocket PC взаимодействует с этими элементами точ- но так же, как приложение для настольного ПК взаимодействует с Windows 2000. ОС Windows СЕ предлагает более широкий набор повторно используемых компо- нентов интерфейса, чем любая другая встраиваемая операционная система, не ис- ключая Palm OS и встраиваемые версии Unix (которые в этом плане не предлага- ют вообще ничего). Эти компоненты абсолютно необходимы для того, чтобы компенсировать ограниченность физического экрана КПК. Для большинства ре- альных приложений необходима надежная система организации многопоточного исполнения. В Windows СЕ программа может создавать новые потоки и синхро- низировать доступ к разделяемым данным. Palm OS не поддерживает нескольких потоков в одной программе. Различные встраиваемые варианты Unix поддержи- вают наличие нескольких процессов, но не потоков, а это приводит к повышен- ным накладным расходам и заметному снижению производительности. И при всем при том Windows СЕ занимает сравнительно немного памяти - всего поряд- ка 4 Мб. Поэтому на типичном КПК приложению остается 8 Мб памяти для раз- мещения исполняемого кода и данных. Многие полнофункциональные приложе- ния для КПК потребляют всего 64 Кб памяти. Все интерфейсы с операционной системой оптимизированы с точки зрения работы с памятью, поэтому приложе- ние может демонстрировать очень высокую эффективность. При работе с тща- тельно спроектированной программой пользователь вообще не замечает никаких задержек.
Основные элементы интерфейса пользователя ЦНИИ 17 совет Описанные в данной книге приемы и повторно используемые компоненты позво- ляют создавать программы с минимальным потреблением памяти и максималь- ной производительностью. Основные элементы интерфейса пользователя В этом разделе мы познакомимся с основными элементами пользовательского интерфейса, предоставляемого работающим на платформе Pocket PC приложе- нием. Эти элементы будут использоваться во всех программах, с которыми мы встретимся на страницах этой книги. Мы также приведем некоторые соображе- ния о том, как правильное применение этих элементов может компенсировать огра- ниченность экрана КПК. ПРИМЕЧАНИЕ Как всегда в программировании, существуют сотни способов конструирования интерфейса пользователя. Простые идеи, изложенные в этом разделе, - это ре- зультат реализации десятков программ для Windows СЕ. Именно они оказались наиболее эффективны в условиях ограниченной площади экрана. При запуске любого приложения для Windows СЕ пользователь видит неко- торый интерфейс. Его основным элементом является окно. Пример простейшего интерфейса приведен на рис. 1.1, где вы видите все важнейшие составные части окна: полосу заголовка, полосу меню с расположенными в ней пунктами меню и клиентскую область. В самой верхней части окна находится полоса заголовка, в которой выводится строка, описывающая назначение программы. Полоса заголовка служит также в качестве средства навигации. По мере того как пользователь взаимодействует со сложным интерфейсом, приложение модифицирует текст заголовка так, чтобы человек знал, где он сейчас находится. В случае программы для Pocket PC интер- фейс по необходимости является иерархическим, в силу ограниченности площа- ди экрана. Если бы полоса меню не использовалась для навигации, то пользова- тель очень скоро перестал бы ориентироваться в программе. Ниже полосы заголовка расположена полоса меню, состоящего из отдельных пунктов. На рис. 1.1 показано менюсодним пунктом Quit (Выйти). Когда пользо- ватель касается этого пункта стилосом, программа завершается и убирает свое окно с экрана. Использование пункта меню в качестве единственного способа за- вершения программы минимизирует место на экране, необходимое для размеще- ния этого важного элемента. В большинстве программ в этой книге полоса меню служит только для перемещения по иерархически организованному интерфейсу. Повторим, что этот подход позволяет сэкономить экранное пространство.
18 Hill Обзор платформы Pocket PC Ниже полосы меню находится основная часть окна - клиентская область, кото- рой управляет программа. Здесь могут отображаться различные элементы управ- ления, например кнопки. В этой же области выполняются все операции рисова- ния. Сюда же программа при необходимости выводит растровые изображения. Обычно размеры клиентской области составляют 145 х 145 пикселей. Если срав- нить это с размером экрана стандартного настольного ПК (1024 х 760), то стано- вится понятно, насколько малая площадь имеется в вашем распоряжении! Полоса заголовка Minimal Dig Program Полоса меню Ж Пункт меню Клиентская область Рис. 1.1. Приложение для Pocket PC с минимальным графическим интерфейсом пользователя СОВЕТ _______________________________________________________ Применяя технику рисования владельцем, вкладки и другие элементы управле- ния, можно создать очень эффективный иерархический интерфейс. В главе 8 приведены примеры и пригодный для повторного использования код, позволяю- щий сконструировать такой интерфейс. Архитектура Windows СЕ В этом разделе мы опишем общую архитектуру операционной системы Windows СЕ. Взаимодействие различных компонентов ОС мы рассмотрим на примере создания файла. Здесь же будет показано, как соотносятся Windows 2000 и Windows СЕ.
Архитектура Windows СЕ На рис. 1.2 представлены три важнейших программных уровня Windows СЕ. На прикладном уровне располагаются все клиентские программы. Каждая такая программа реализует определенные функции, представляющие интерес для кон- кретной группы пользователей, например это может быть программа для выявле- ния изъянов в производственном процессе, которая осуществляет анализ и дос- туп к базе данных. Любое клиентское приложение взаимодействует с элементами, расположенными на следующем уровне, - GWES (Graphics, Windowing and Event Subsystem - подсистема управления графикой, окнами и событиями). На рис. 1.2 видно, что GWES - это защищенная подсистема. Защищенная подсистема обес- печивает управляемый интерфейс между всеми приложениями и базовыми ме- ханизмами операционной системы. Как и всякий интерфейс, подсистема GWES решает две основные задачи: доставляет введенные пользователем данные прило- жению, а выводимые приложением данные - аппаратуре дисплея и операционной системе Windows СЕ. Для выполнения этих функций GWES обращается к раз- личным элементам на следующем уровне - исполняющей подсистеме СЕ (СЕ Exe- cutive). В действительности СЕ Executive состоит из целого ряда компонентов, взаи- модействующих между собой в ходе выполнения различных операций. Они тоже представлены на рис. 1.2, это менеджер объектов (Object Manager), менеджер про- цессов (Process Manager), менеджер памяти (Memory Manager), менеджер ввода / вывода (I/O Manager) и вездесущее ядро.
20 Illi Обзор платформы Pocket PC СОВЕТ 4 Представленными на рис. 1.2 элементами исполняющей подсистемы ОС Windows СЕ не исчерпывается. Они лишь отвечают за выполнение основных операций ОС. По существу, операционная система Windows СЕ построена на базе объектов. Любой ресурс, будь то процесс, поток или файл, который создает или использует программа, выглядит для нее как объект со скрытой структурой. Задача менедже- ра объектов - связать структуры данных, расположенные в памяти операционной системы, с описателем (или идентификатором) этого объекта. Тем самым клиент- ское приложение может обращаться к ресурсу только с помощью четко опреде- ленных методов, которым передается описатель объекта и которые проверяют корректность входных данных. Напрямую модифицировать структуры данных, управляемые операционной системой, приложение не может. За счет этого повы- шается надежность как приложения, так и самой операционной системы. Менеджер процессов вступает в игру, когда пользователь запускает приложе- ние. Этот компонент ОС создает начальный поток приложения, который называет- ся основным потоком, и подготавливает ряд важных структур данных, например кучу в памяти (memory heap). Для этого менеджер процессов взаимодействует с другими компонентами: ядром и менеджером памяти. Выделение, освобождение и учет физической памяти - задачи менеджера па- мяти. Когда менеджер процессов или клиентское приложение запрашивают па- мять, менеджер памяти находит свободную память, помечает ее как занятую и пе- редает в распоряжение приложения. В ходе освобождения памяти выполняется обратная процедура, в результате чего возвращенная системе память становится доступной другим приложениям. Все ресурсы ввода / вывода и операции, касающиеся физических устройств, например файлы, последовательный порт или сетевые порты находятся под управ- лением менеджера ввода / вывода. Когда клиент запрашивает файл, менеджер ввода / вывода выполняет ряд операций, аналогичных тому, что делает менеджер памяти. В случае Pocket PC пространство для размещения файлов находится во внутренней памяти КПК, а не на внешнем устройстве. Поэтому файловый менед- жер обращается к менеджеру памяти для выделения необходимой памяти, а затем создает структуры данных для управления файлом, например указатель текущей позиции. Самая важная часть операционной системы Windows СЕ - это ядро. Его основ- ная задача - управлять существующими потоками и планировать их исполнение. Когда менеджер процессов запрашивает создание основного потока приложения, именно ядро организует поток и создает необходимые структуры данных. Разуме- ется, память для этих структур запрашивается у менеджера памяти. Для любого потока необходимо два набора структур данных. Первая позволяет ядру следить за состоянием исполняемого процессором потока, в частности сохранять и восста- навливать счетчик команд. Вторая структура - это стек, в котором поток разме- щает локальные переменные.
Архитектура Windows СЕ 21 Частью ядра является также планировщик потоков. Он отвечает за то, чтобы каждый поток получал справедливую долю времени центрального процессора в соответствии со своим приоритетом. После того как поток проработает в тече- ние одного кванта времени, обработчик прерываний передает управление плани- ровщику потоков, который решает, какой поток будет исполняться следующим, и запускает его, предварительно сохранив состояние текущего потока. ПРИМЕЧАНИЕ Описанные выше компоненты присутствуют и в исполняющей подсистеме Windows 2000. Объекты, управляемые СЕ Executive, - это сокращенные вер- сии объектов в Windows 2000. Разместив сокращенный вариант Windows 2000 в оперативной памяти Poc- ket PC, Microsoft создала полнофункциональную и при том очень мощную опера- ционную систему для поддержки выполнения пользовательских программ. Создание файла в клиентском приложении - эти пример типичного взаимо- действия между компонентами ОС Windows СЕ. Последовательность таких взаи- модействий изображена на рис. 1.3. Каждая строка представляет взаимодействие между двумя элементами. Элемент в колонке «Инициатор» начинает взаимодейст- вие, а элемент в колонке «Исполнитель» является отвечающей стороной. Суть взаимодействия отражена в колонке «Описание». Инициатор Исполнитель Описание | Клиентское приложение Подсистема GWE Создать файл Подсистема GWE Менеджер ввода / вывода Запрос о создании файла менеджеру ввода /вывода Менеджер ввода / вывода Менеджер объектов Создать файловый объект Менеджер объектов Менеджер ввода / вывода Описатель файлового объекта Менеджер ввода / вывода Менеджер памяти Структуры данных, описывающие файл Менеджер памяти Менеджер ввода / вывода Указатель на объект в памяти Менеджер ввода / вывода Подсистема GWE Описатель файлового объекта Подсистема GWE Клиентское приложение Описатель файлового объекта Рис. 1.3. Последовательность операций при создании файла В самом начале клиентское приложение вызывает функцию CreateFile из ин- ерфейса прикладных программ Win32 API. Эта функция обращается к подсисте-
22 МИН Обзор платформы Pocket PC ме GWE. В ответ подсистема GWE передает запрос менеджеру ввода / вывода исполняющей подсистемы Windows СЕ Executive. Для этого GWE должна просто проверить корректность переданных ей аргументов и передать необходимые аргументы функции OICreateFile, входящей в состав менеджера ввода / вывода. Менеджер ввода / вывода должен решить две задачи. Сначала он обращается к менеджеру объектов для создания объекта, представляющего конкретный файл. В результате в глобальное пространство имен, управляемое менеджером объек- тов, помещается объект, описывающий файл. После того как менеджер ввода / вывода получит описатель этого объекта, он должен запросить физическую па- мять для размещения содержимого файла. Выделение памяти - это прерогатива менеджера памяти, так что менеджер ввода / вывода должен обратиться к нему. Оставшиеся взаимодействия на рис. 1.3 - это возврат описателя файлового объекта вверх по цепочке вызовов клиентскому приложению. Во всех последую- щих операциях с файлом - чтении, записи, проверке достижения конца файла - используется этот описатель. Он однозначно идентифицирует открытый файл для всех компонентов исполняющей подсистемы. Клиентское приложение может выполнять над файлом только те операции, которые поддерживаются функция- ми, принимающими в качестве первого аргумента описатель файла. Внутри подсистемы GWE Клиентское приложение для Pocket PC взаимодействует с операционной си- стемой Windows СЕ через подсистему управления графикой, окнами и события- ми (GWE). Если программист хорошо понимает внутреннюю организацию и ра- боту этой подсистемы, то он сможет спроектировать эффективное приложение. На рис. 1.4 показана архитектурная организация подсистемы GWE. Ее важными элементами являются очереди, а также компоненты GDI, WINDOW и USER. Первым делом мы рассмотрим компонент «системная очередь». Драйверы всех устройств помещают в эту очередь сообщения, содержащие информацию о действиях пользователя. Компонент USER перемещает эти сообщения в оче- редь сообщений потока. Внутри компонента USER работает специальный поток, который называется Raw Input Thread (RIT - поток необработанного ввода). Он просто следит за появлением новых сообщений в системной очереди. Как только в нее поступает новое сообщение, поток RIT извлекает его, определяет, кому оно предназначено, и помещает сообщение в очередь получателя. Напомним, что основной поток приложения создается ядром; именно в оче- редь этого потока помещается сообщение. На основе описателя окна, включаемо- го в состав сообщения драйвером устройства, поток RIT в компоненте USER мо- жет без труда определить получателя сообщения. Клиентское приложение для Pocket PC извлекает сообщение из очереди пото- ка и каким-то образом реагирует на него. Если в ходе обработки сообщения нужно обновить окно или находящиеся в нем элементы управления, то приложение вы- полняет функции, принадлежащие компоненту WINDOW. Если же реакция под- разумевает выполнение операций рисования, то приложение обращается к ком- поненту GDI (Graphics Device Interface - интерфейс графических устройств).
Рис. 1.4. Внутреннее устройство подсистемы GWE СОВЕТ Подробное описание функций GDI приведено в разделе «Обзор интерфейса гра- фических устройств» ниже в этой главе. На рис. 1.5 показана последовательность взаимодействий между клиентским приложением и различными компонентами подсистемы GWE. Она начинается со щелчка мышью внутри окна приложения. Инициатор Исполнитель Описание Драйвер мыши Системная очередь мышью Пользователь щелкает Системная очередь Компонент USER сообщение Поток RIT извлекает Компонент USER Очередь сообщений потока сообщение в очередь Поток RIT помещает Очередь сообщений потока Основной поток приложения Приложение извлекает и обрабатывает сообщение Основной поток приложения Компонент GDI в клиентской области Приложение рисует Рис. 1.5. Последовательность операций при взаимодействии с пользователем
Обзор платформы Pocket PC 24 Когда пользователь щелкает мышью, возникает прерывание, и управление передается драйверу мыши. Драйвер выясняет, какое окно в данный момент вла- деет фокусом, а затем создает сообщение, в которое включает описатель этого окна, положение курсора внутри клиентской области, а также временной штамп события. Подготовленное сообщение драйвер помещает в системную очередь. За- тем вступает в игру поток RIT из компонента USER подсистемы GWE. Рано или поздно планировщик, входящий в состав ядра, передаст процессор в распоря- жение потока RIT. Этот поток исполняет цикл, в котором проверяется состояние системной очереди. Сообщения извлекаются из очереди в порядке «первым при- шел - первым обслужен». В какой-то момент сообщение о щелчке мышью достиг- нет начала системной очереди, и в этот момент поток RIT удалит его. Удалив сообщение из очереди, поток RIT разбирает его, выделяет окно-полу- чатель, по описателю этого окна находит соответствующую очередь сообщений потока и помещает в нее сообщение. Основной поток клиентского приложения также исполняет цикл выборки сообщений из очереди и их последующей обра- ботки. Когда сообщение о щелчке мышью оказывается в начале очереди, прило- жение удаляет его и начинает обрабатывать. Обработка может, например, заклю- чаться в рисовании отрезка прямой, соединяющего некоторую начальную точку с точкой, в которой имел место щелчок мышью. Но, конечно, конкретная реакция целиком и полностью зависит от приложения. Для того чтобы клиентское приложение могло работать в описанном контекс- те, оно должно быть определенным образом организовано. Единая для всех при- ложений структура выглядит так: вывести главное окно ; while не конец { выбрать сообщение из очереди потока ; обработать сообщение ; } Программа, построенная по такой схеме, называется событийно-ориентиро- ванной. Приложение реагирует на события, возникающие, как правило, в процес- се взаимодействия с пользователем. Причиной для появления сообщений в очере- ди приложения могут быть нажатия на клавиши, щелчки мышью и целый ряд других событий. Обзор интерфейса графических устройств (GDI) Когда приложение хочет что-то нарисовать в клиентской области, оно отдает команды компоненту GDI. На рис. 1.6 показаны ассоциированные с GDI элементы. При пользовании службами GDI необходимо проводить различие между рисо- ванием графики (graphics drawing) и отображением графики (graphics displaying). Говоря о рисовании, мы имеем в виду команды рисования объектов на виртуаль- ном холсте с помощью тех или иных инструментов. Инициирование таких дейст- вий возлагается ла клиентскую программу. Отображение же гр * " г'!' ’ ел
Обзор интерфейса графических устройств 1111 GDI. Под этим понимаются те операции, которые выполняют Windows СЕ и драйверы устройств для физического вывода изображения на виртуальный холст. Рисование графики включает ряд важных концепций, как то: виртуальное пространство рисования, операция рисования и инструмент рисования. Клиентская программа рисует исключительно в виртуальном или логическом пространстве. Оно весьма велико. Аргументами различных команд рисования служат 32-разрядные целые числа, следовательно, диапазон по каждой оси коор- динат простирается от -231 до +231. Немало! Рисование графики Логические координаты Операции рисования Инструменты рисования Контексты устройства Окно Отображение графики Экранные координаты Клиентские координаты Режимы отображения Драйверы устройств Клиентская область Порт просмотра Типы аргументов накладывают ограничение на ширину диапазона Рис. 1.6. Элементы графической модели Windows СЕ GDI предоставляет в распоряжение программы набор операций или команд рисования, например прямой линии, прямоугольника, эллипса и скругленного прямоугольника. Имеется также набор инструментов рисования, например перья и кисти. У инструментов есть разнообразные атрибуты, управляемые программой. Приступая к рисованию, программа сначала набирает комплект инструмен- тов. Этот комплект формально называется контекстом устройства. Команда ри- сования включает в себя описатель комплекта инструментов, конкретную опера- цию рисования и прочие необходимые для рисования аргументы. Всю эту информацию приложение передает GDI с помощью специальных функций, сово- купность которых и определяет возможности рисования. Типичная программа для Pocket PC выдает последовательности команд рисо- вания. Полученные команды GDI сохраняет во внутреннем кэше или буфере. Когда программа сообщает, что больше команд не будет, начинается собственно
26 1111 Обзор платформы Pocket PC рисование. Последовательность операций отображения и отсечения преобразует команды рисования в логическом пространстве в изображения внутри клиент- ской области физического окна приложения. В предположении, что какие-то команды пережили все операции отобра- жения и отсечения, GDI приступает к рисованию в видеобуфере физического устройства. Но самостоятельно он этим не занимается, а взаимодействует с драй- вером устройства, который и выводит пиксели в буфер кадров. Драйвер устройст- ва преобразует цвета отдельных пикселей в комбинации цветов, которые устройство способно отобразить, и помещает их в буфер кадров. Далее аппаратура обрабатывает этот буфер, в результате чего внутри клиентской области на физи- ческом экране появляется изображение. Использование во всех функциях рисования логических координат позволяет приложению ничего не знать о характеристиках физического оборудования. При переносе приложения на другой КПК разработчику не нужно модифицировать программу с учетом других размеров экрана и числа цветов. Это преобразование возлагается на драйвер устройства, так что программа может работать на разных КПК без существенных переделок. — ПРЕДОСТЕРЕЖЕНИЕ_____________________________________________ Jv * - удД Если физический размер области отображения изменяется, то разработчик при- ELjT'x. уш ложения должен изменить размеры окна и аргументы функций рисования, так чтобы окно занимало большую или меньшую площадь на экране. Если же прило- жение переносится с черно-белого экрана на цветной, то обычно никаких изме- нений в код рисования вносить не приходится. Чуть выше мы говорили о том, что контекст устройства - это не что иное, как комплект инструментов, которые GDI применяет при выполнении конкретной операции рисования. На рис. 1.7 приведен перечень инструментов, содержащихся в контексте устройства. Это перья, кисти, шрифты и ряд других ресурсов и пара- метров, необходимых для рисования. Помимо перечня инструментов, входящих в состав контекста устройства, на этом рисунке представлены еще и подразумеваемые по умолчанию значения. Но надо признать, что при использовании одного лишь черного пера для рисования текста и графики интерфейс получился бы не слишком выразительным. Поэтому в распоряжении программиста имеется широкий набор настроек для каждого инструмента. Если приложение хочет использовать для какого-то ин- струмента значение, отличное от умалчиваемого, то может вызвать простую функ- цию, которое подставит в описание инструмента другое значение. Например, можно сказать, что все операции рисования должны выполняться красным, а не черным пером. После того как все красные линии нарисованы, можно восстано- вить стандартный цвет пера в контексте устройства. Это необходимо, поскольку система поддерживает лишь ограниченное количество контекстов устройств, так что они повторно используются разными приложениями.
Обзор интерфейса графических устройств 11П1П 27 Содержит инструменты, I влияющие на все операции рисования Цвет переднего плана и фона <- Перо <*- Значения по умолчанию — Белый Кисть <— -- — BLACK_PEN WHITE_BRUSH Режим рисования Шрифт Межсимвольный промежуток «— R2_COPYPEN Область отсечения *__________________ Преобразование из окна в порт просмотра Используются драйверами устройств для вывода на физический экран SYSTEM FONT Рис. 1.7. Инструменты рисования, хранящиеся в контексте устройства СОВЕТ Если приложение не восстанавливает исходную конфигурацию инструментов в контексте устройства, то следующий пользователь того же контекста получит модифицированные инструменты. Из-за этого программа может отображать гра- фику не так, как задумал автор, что, конечно, не понравится пользователям. Еще один важный аспект, касающийся инструментов в контексте устройства, состоит в том, что эти инструменты виртуальны, поскольку точную семантику каждого инструмента определяет драйвер. Предположим, что приложение заменило черное перо красным. Это еще не означает, что линии действительно будут рисоваться красным цветом. Причина в аппаратной независимости GDI. Драйвер и GDI совместно решают, что такое «красный», принимая во внимание возможности видеоаппаратуры. Так, на моно- хромном дисплее появится черная линия, следовательно, в этом случае виртуаль- ное красное перо на деле является черным. Но клиентская программа будет рабо- тать нормально, несмотря на то, что видеоаппаратура не может отобразить красные пиксели. Если бы не аппаратная независимость GDI в Windows СЕ, то программисту пришлось бы заменить все команды рисования красным цветом на рисование черным или на какие-то другие команды, поддерживаемые аппарату- рой конкретного Pocket PC.
28 MMIIIII Обзор платформы Pocket PC Логическая структура программы для Windows Напомним, что все программы для Pocket PC имеют следующую структуру: вывести главное окно ; while не конец { выбрать сообщение из очереди потока ; обработать сообщение ; } Если реакция на сообщение подразумевает обращение к GDI, то приложение помещает все необходимые инструменты в контекст устройства и посылает нуж- ные команды рисования в логической системе координат. На первый взгляд, эта логика представляется совсем простой. Но, к несчастью, реализовывать ее приходится в реальной программе, а для этого необходимы раз- личные ухищрения. На рис. 1.8 перечислены основные функции API, поддержи- вающие описанную выше структуру программы. Особенно важно правильно реализовать взаимодействие с компонентом USER, работу с очередью сообщений основного потока и ветвление по типу сообщения. В любой программе для Pocket PC должны быть такие две функции: □ WinMain организует цикл выборки сообщений из очереди; □ WndProc обрабатывает отдельные сообщения. Рис. 1.8. Реализация событийно-ориентированных программ Любое приложение для Pocket PC должно явно включать эти две функции, причем их сигнатуры также фиксированы. В понятие сигнатуры функции входят ее имя, тип возвращаемого значения и типы всех аргументов. Программа для Windows СЕ не откомпилируется, если хотя бы одна из этих двух функций отсутст- вует или имеет неправильную сигнатуру. На этапе инициализации приложения необходимо зарегистрировать оконную процедуру с именем WndProc. Функция WmMain в цикле вызывает функцию GetMessage, входящую в состав API. GetMessage запрашивает у компонента
Логическая структура программы для Windows ||||НМ 29 USER следующее сообщение из очереди основного потока. Внутри цикла WinMain передает полученное сообщение другой функции API - DispatchMessage, задача которой - разобрать сообщение и передать хранящиеся в нем данные зарегистри- рованной процедуре WndProc. Обработка сообщений в программе Внутри WndProc имеется предложение switch, в котором вызывается обра- ботчик конкретного сообщения. Этот обработчик вызывает функции API, необхо- димые для взаимодействия с GDI. Все сообщения, предназначенные главному окну приложения, поступают в очередь основного потока. Обработка большинст- ва из них сводится к стандартному поведению, определенному в компоненте USER, но при необходимости программа может и переопределить это поведение. Если поведение по умолчанию устраивает, то в ветви default предложения switch следует вызвать функцию DefWindowProc, передав ей извлеченное из очереди сообщение. СОВЕТ Согласно онлайновой документации, функция DispatchMessage заставляет ком- понент USER вызвать зарегистрированную процедуру WndProc. Предположи- тельно это связано с тем, что USER стремится освободить программу от работы по разбору сообщения и поиску адреса зарегистрированной процедуры. Однако такое объяснение скрывает истинную причину, по которой клиентское прило- жение обязано вызвать DispatchMessage. После возврата из WndProc функция DispatchMessage проверяет, что приложение правильно выполнило все задачи, связанные с рисованием, а еслиз/тоне так, исправляет упущения. Каждое взаимодействие с клиентской программой, будь то со стороны пользо- вателя или самой ОС Windows СЕ, включает отправку сообщения программе. Компонент USER поддерживает сотни видов сообщений. На рис. 1.9 показаны те из них, что обрабатываются программами для Pocket PC чаще всего. В левой части представлены коды сообщений (или символы). Сообщение попа- дает в очередь потока при определенных условиях. Эти условия показаны в пра- вой части рисунка. Большая часть сообщений связана с работой аппаратуры или механизма ото- бражения графики. Наиболее интересны те, что относятся к взаимодействию с пользователем: WM_COMMAND и WM_NOTIFY. Они извещают о том, что пользователь обратился к тому или иному компоненту графического интерфейса. Например, когда пользователь выбирает пункт из выпадающего меню, то в конец очереди основного потока помещается сообщение. Рано или поздно это сообще- ние будет передано соответствующему обработчику в WndProc, и он выполнит в ответ те или иные действия. В потоке сообщений, обрабатываемых процедурой WndProc, отчетливо про- глядывает паттерн «время жизни». В начале работы приложение получает сооб- щение WM_CREATE. В процессе взаимодействия с пользователем приложе-
30 nr Обзор платформы Pocket PC WM.CREATE «-- WMPAINT «--- WM_COMMAND WM_NOTIFY WM_KEYDOWN WM.KEYUP *“ LBUTTONDOWN Й-MOUSEMOVE < M.LBUTTONUR WM_M0VE WM.SIZE WM_CL0SE «-- WM_DESTROY < • Окно только что создано (конструктор) Окно нужно перерисовать Пользователь взаимодействует с приложением Имела место операция с клавиатурой Имела место операция с мышью Изменилось положение или размер окна Запрос на выход из приложения Окно будет уничтожено (деструктор) Рис. 1.9. Представление наиболее часто возникающих событий в виде сообщений ние получает различные сообщения, которые либо изменяют состояние главно- го окна (WM_M0VE и WM_SIZE), либо извещают о действиях пользователя (WM_COMMAND, WMKEYDOWN и WMBUTT0ND0WN). Непосредствен- но перед завершением программа получает сообщение WM_DESTROY. Сообще- ния WM_CREATE и WM_DESTROY особенно полезны тем, что определяют точки, в которых программа может захватить и освободить ресурсы, например, физические устройства, файлы и базы данных. Обновление клиентской области окна В описанной выше схеме разработки событийно-ориентированной програм- мы для Pocket PC есть место для обновления клиентской области главного окна приложения. На рис. 1.10 показано, как это происходит. Обновление клиентской области обычно происходит в результате обработки некоторых сообщений, после чего программа явно извещает систему о том, что на экране произошли изменения. Для этого служит функция InvalidateRect. На рис. 1.10 изображена последовательность событий после нажатия пользова- телем кнопки. В какой-то момент WndProc получит сообщение WM_LBUTTON- DOWN. Его обработчик извлечет из сообщения координаты курсора мыши и со- хранит их в буфере для дальнейшего использования. Перед выходом обработчик сообщения известит систему о том, что необходимо обновить окно, для чего вызо- вет функции InvalidateRect. Через некоторое время WndProc получит сообщение WM_PAINT, служащее сигналом для перерисовки. Обработчик это1 о сообт цИя
Резюме ЦНИИ 31 извлечет координаты мыши из буфера и передаст их командам рисования из ком- понента GDI. Пользователь нажимает кнопку Окно обновлено Рис. 1.10. Управляемая событиями схема обновления клиентской области Резюме В этой главе мы рассмотрели основы архитектуры ОС Windows СЕ, а также требования, предъявляемые к приложению, которое должно работать под управ- лением этой ОС. Эти знания понадобятся для понимания последующих глав. Вот что следует запомнить: □ Windows СЕ - это сокращенная версия Windows 2000; □ сообщения, получаемые программой, - это абстракция входных данных; □ для генерирования выходной информации программа пользуется коман- дами рисования и применяет инструменты рисования; □ абстрагирование обработки входной и выходной информации позволяет писать программы, не зависящие от платформы; □ в любой программе для Pocket PC должны быть функции WinMain и WndProc, а также обработчики сообщений; □ при обработке сообщений часто требуется рассматривать нескольких со- бытий и явно извещать систему о необходимости тех или иных действий.
Глава 2. Типичная программа для Pocket PC В этой главе мы рассмотрим основы реализации программ для Pocket PC. На примере простой программы будут продемонстрированы все требования. Затем мы покажем, как преобразовать программу для настольного компьютера в про- грамму для Windows СЕ, проанализируем возникающие при этом проблемы. В ходе анализа будут вскрыты изъяны, присущие прямолинейному подходу, и заложены основы каркаса, который будет подробнее рассмотрен в следующей главе. Рассматриваемую программу реализовать не слишком сложно. По существу, она содержит лишь шаги, необходимые для запуска и завершения любого при- ложения на платформе Windows СЕ. В ней мы увидим функции WinMain и WndProc, упомянутые в предыдущей главе. Кроме того, интерфейс будет содер- жать единственную кнопку для выхода из программы. В самом начале работы программа создаст эту кнопку путем обращения к функции, входящей в состав Windows API. Обработчик нажатия на нее завершит приложение. Уникальные особенности программ для Windows СЕ При разработке любой программы для Windows СЕ нужно учитывать требо- вания сверх тех, что были описаны в предыдущей главе. Они проистекают из того факта, что Windows СЕ - сокращенная версия Windows 2000, и это налагает опре- деленные ограничения. Точнее, программа должна удовлетворять следующим дополнительным тре- бованиям: □ приложению доступно лишь подмножество функций Win32 API; □ приложению доступно лишь подмножество функций из стандартной биб- лиотеки ANSI С времени исполнения; □ для некоторых операций необходимы совершенно другие функции Win32 API; □ некоторые функции Win32 API ведут себя иначе; □ все строки должны быть представлены в кодировке Unicode, а не ASCII. Хотя поначалу эти требования могут обескуражить, но по мере работы следо- вание им войдет в привычку.
Уникальные особенности программ !!! 33 СОВЕТ ___ . Каркас библиотеки, который мы начнем разрабатывать в следующей главе, скры- вает большую часть специфических требований и позволяет писать программы, работающие как на настольном ПК, так и на Pocket PC. В интерфейсе Win32 API для Windows 2000 насчитывается более 4000 функ- ций. Среда Embedded Visual C++ 3.0 предоставляет программе для Pocket PC примерно 2000 функций. Как будет видно из последующих глав, этого вполне до- статочно для создания любого приложения для Pocket PC. Отсутствие некоторых возможностей в Embedded Visual C++ бросается в гла- за. Прежде всего это поддержка консольных приложений, безопасности и сер- висов, работающих в фоновом режиме. Есть и еще кое-какие нереализованные функции, например SetWindowPos. Но поскольку перемещение окон не входит в графический интерфейс пользователя, поддерживаемый Windows СЕ, то отсутст- вие этой функции не должно вызывать удивления; это было бы лишь пустой тра- той ценной памяти. ПРИМЕЧАНИЕ Microsoft предлагает стандартную конфигурацию средств, поддерживаемых Win32 API на платформе Windows СЕ. Но производители оборудования, в частнос- ти фирма Casio, вольны модифицировать эту конфигурацию. Для этого предназ- начена программа Platform Builder. В результате состав API для конкретного КПК может отличаться от стандартного. Такому же сокращению подверглась и стандартная библиотека ANSI С, под- держиваемая компилятором Embedded Visual C++. Самое очевидное изъятие - это все функции, объявленные в заголовке stdioh. Это логично, так как они каса- ются ввода / вывода на консоль, а этого механизма в Windows СЕ нет. Отсутству- ет также ряд функций, обычно объявляемых в заголовке stdlib.h. Из тех, что могли бы оказаться полезны, отметим функции atof (для преобразования текстовых строк в число с плавающей точкой), calloc (для выделения и обнуления памяти) и bsearch (двоичный поиск). Впрочем, при необходимости эти задачи можно ре- шить другими способами или найти переносимый код в сети. Иногда бывает так, что для реализации задачи приходится применять непри- вычные функции. Самый очевидный пример - это функции для вывода меню. В версии Visual C++ для настольных ПК для вывода меню в клиентской области служат функции LoadMenu и SetMenu. Тот же эффект в Windows СЕ достигается с помощью функций CommandBar_Create и CommandBar_InsertMenubar. Разли- чие объясняется тем, что меню является частью полосы команд, в которой могут располагаться и другие элементы управления, например кнопки и списки. Такое использование полосы команд позволяет строить более сложные и удобные ин- терфейсы. Однако за гибкость приходится платить - в данном случае другим на- бором функций для создания меню.
34 Illi Типичные программы для Pocket PC Поведение некоторых поддерживаемых функций в Windows СЕ изменилось. Например, это относится к функциям ReadFile и WriteFile, предназначенным для чтения и записи файла. При выводе строки в файл с помощью функции WriteFile для настольного ПК она записывается в файл без изменения. С помощью любого текстового редактора, например Notepad или Word Pad, этот файл можно открыть и увидеть ASCII-символы. Но в Windows СЕ вы увидите «мусор»; дело в том, что файл содержит строку в кодировке Unicode. Перед записью в файл программа для Pocket PC должна преобразовать строку в ASCII, а затем передать ее функции WriteFile. Быть может, самое существенное различие между программами для настоль- ного ПК и Windows СЕ - это необходимость представлять все текстовые строки в кодировке Unicode. Unicode - двухбайтовая кодировка. Кодирование каждого символа двумя байтами значительно расширяет диапазон представимых симво- лов. Для сравнения - в кодировке ASCII каждый символ представляется одним байтом (8 битов), что дает всего 256 различных символов. Хотя для латинского алфавита этого вполне достаточно, но, например, для японского алфавита кандзи, в котором свыше 5000 иероглифов, явно не хватает. В двухбайтовой же кодировке можно представить 65 536 символов. Каждый символ в кодировке Unicode - это кодовая точка (code point). Для разных языков отведены различные диапазоны кодовых точек. Так, первые и по- следние 256 кодовых точек зарезервированы для ASCII-кодов символов латиницы. Сейчас примерно половина всех кодовых точек еще не распределена по языкам. ПРИМЕЧАНИЕ Наличие лишь функций для работы с Unicode-строками - еще одно свидетель- ство того, что Windows СЕ - сокращенная версия Windows 2000. Внутри Windows 2000 текст всегда представляется в кодировке Unicode. Если приложение вызы- вает функцию, принимающую в качестве аргумента ASCII-строку, то Windows 2000 сначала преобразует ее в Unicode, выполняет операцию, а затем преобразу- ет результирующую строку назад в кодировку ASCII. Для программ, в которых используются символьные строки, в Windows СЕ предусмотрены два набора функций и макросов. Один из них принимает в качест- ве аргументов Unicode-строки. Основной тип данных для таких функций - это WCHAR. Здесь буква W означает wide (широкий). Широкий символ занимает два байта и точно соответствует кодировке Unicode. Во втором наборе функций и макросов применяются переносимые типы, и базовым является тип TCHAR. По определению, ТCHAR отображается либо на стандартный тип char для представ- ления ASCII-символов, либо на тип WCHAR - в зависимости от целевой плат- формы. Visual C++ решает, чему соответствует тип TCHAR, ориентируясь на флаги, заданные при компиляции программы. Таким образом, за счет использова- ния переносимого типа мы можем автоматически получить программу, предназ- наченную для выбранной платформы
IIIIMIBi Тип TCHAR 35 СОВЕТ Ради обеспечения кросс-платформенного тестирования (о нем речь пойдет в сле- дующей главе) во всех примерах из этой книги используются тип данных TCHAR и соответствующие ему функции и макросы. Тип TCHAR - основа переносимого механизма обработки строк К сожалению, для работы со строками переносимым образом нужно прило- жить некоторые усилия. Ниже перечислены шаги, которые нужно выполнить для перехода от обработки ASCII-строк к работе с типом TCHAR. 1. Включить заголовочный файл <tchar.h>. 2. Объявить все строковые переменные как имеющие тип TCHAR. 3. Для указателей на строки символов использовать тип TCHAR* или LPTSTR. 4. Погрузить строковые литералы в макрос_ТЕХТ(). 5. Использовать переносимые функции для работы со строками, например _tcscpy вместо strcpy. 6. При вычислении объема памяти для массивов символов умножать число символов на sizeof(TCHAR). Автоматическое преобразование кодировки может сказаться на арифметике указателей. Во многих программах для перемещения по буферу применяются арифметические операции над указателями. Впрочем, перекодировка на этапе компиляции на это не влияет. Все вычисления с указателями выполняются кор- ректно с учетом целевой платформы и выбранного представления символов. Процедуру преобразования можно проиллюстрировать на простом примере. В следующем фрагменте обрабатываются исключительно ASCII-строки: ♦include <string.h> char Stringl[50]; LPSTR String?; String2 = (LPSTR) malloc(30 * sizeof(char) ); strcpy( Stringl, «abcdef» ); strcpy( Strings, «xxyyzz» ); free( Strings ); После преобразования код принимает такой вид: ♦include <tchar.h> // Шаг 1 TCHAR Stringl[50]; // Шаг 2 LPTSTR strings,- // Шаг 3 strings = (LPTSTR' nalloc(20 * sizeof(TCHAR) ); // Шаг 6
36 III! Типичные программы для Pocket PC tcscpyf Stringl, ____TEXT («abcdef») ); // Шаги 5, 4 tcscpyf String2, ___TEXT(«xxyyzz») ); // Шаги 5, 4 free( String2 ); Комментарий в конце каждой строки показывает, какое к ней было применено правило преобразования. Рассмотрим, к примеру, первую из строк, начинающихся с _tcscpy. Правило 5 говорит, что вместо функции strcpy надо использовать _tcscpy, а правило 4 - что строковый литерал «abcdef» нужно окружить макросом___TEXT. СОВЕТ Хотя Windows СЕ поддерживает главным образом Unicode-функции, но есть не- сколько функций для работы с ASCII-строками. Приложение, которое ими пользу- ется, может объявить переменную типа char. Например, перед записью в файл можно преобразовать Umcode-строку в кодировку ASCII с помощью функции wcstombs, которая принимает на входе строку в кодировке Unicode, а возвращает ASCII-строку в переданном буфере типа char. Однако такие функции, как strcpy для копирования ASCII-строк, не поддерживаются и не компилируются. Анатомия простой программы для Windows В этом разделе мы рассмотрим простую программу для Windows на концеп- туальном уровне. В частности, будет затронут ряд деталей реализации, которые еще не обсуждались: конкретный графический интерфейс пользователя; структу- ры данных, необходимые любой программе, работающей под Windows; формат сообщений, которые Windows посылает приложению, и двоичная сигнатура про- граммы. Все это нужно для понимания того, как пишутся и работают программы на платформе Windows СЕ. На рис. 2.1 изображен графический интерфейс простой программы, которую мы разработаем в этой главе. Окно программы содержит полосу заголовка, в которой выводится надпись «HelloWorld Program». В верхнем левом углу клиентской области находится кнопка. При нажатии на нее программа завершается. Многие программы для Windows еще выводят в правом верхнем углу полосы заголовка иконки для управ- ления сворачиванием и разворачиванием окна, но здесь этого нет. Такую возмож- ность Windows СЕ поддерживает, но в программах из этой книги она не использу- ется. За счет этого удается более строго контролировать интерфейс пользователя. Пользователь может либо работать с программой, либо выйти из нее. Сворачива- ние и разворачивание хороши для настольных ПК, где можно работать одновре- менно с несколькими программами. Но в такой среде, как Windows СЕ, человек обычно занимается одной задачей, для решения которой нужно одно приложение, так что эта функциональность просто излишня. Дополнительный побочный эффект сворачивания заключается в том, что свернутая программа может оставить в неопределенном состоянии такие ресур- сы, как файлы и последовательные порты. При работе с Pocket PC из-за малень-
Анатомия простой программы для Windows HelloWorld Program > OK The OK button terminates the program http: //all - ebooks. com Рис. 2.1. Графический интерфейс простой программы кого экрана пользователь может свернуть программу, а потом забыть про нее. Но когда программа свернута, она не закрывает файлы и не освобождает ресурсы, а в результате другие программы, нуждающиеся в тех же ресурсах, могут работать неправильно. Если аккумулятор разрядится, то файл так и останется незакры- тым, а это может привести к потере данных. Всех этих нежелательных эффектов можно избежать, если приложение не поддерживает сворачивания. В любой программе для Windows есть ряд важных структур данных. Речь, в частности, идет об определении и создании оконного класса и обработке полей сообщения. На уровне API оконный класс определяется путем заполнения неко- торой таблицы. СОВЕТ Помещение данных в таблицу заметно сокращает время разработки. В этом слу- чае отладка обычно не нужна. Д ведь на отладку логики программы, разыменова- ния указателей и прочих деталей уходят многие часы. Первая структура, которую мы рассмотрим, имеет тип WNDCLASSEX. На рис. 2.2 представлены ее поля. Структура WNDCLASSEX определяет общие свойства оконного класса. В верхней части списка показаны два самых важных ее поля. Поле ClassName за- дает имя класса приложения. С его помощью Windows ссылается на оконный класс приложения. Второе поле - WindowProcedure (оконная процедура) - со- держит указатель на функцию, обрабатывающую все сообщения, посылаемые окну конкретного класса. Остальные поля мы обсудим по ходу изложения. После того как общие характеристики оконного класса определены, програм- ма создает конкретный экземпляр этого класса. При создании экземпляров или оконных объектов заполняется вторая таблица. Атрибуты оконного объекта так-
38 Illi Типичные программы для Pocket PC же показаны на рис. 2.2. Первое поле структуры совпадает с определенным ранее именем класса. Это позволяет Windows связать с конкретным оконным объектом общие характеристики класса. Остальные поля таблицы относятся к конкретно- му оконному объекту. Ясно, что положение и размер свои для каждого окна. йОбщие характеристики) Общие характеристики плюс уникальные атрибуты i WNDCLASSEX ClassName -*— Оконная процедура Описатель экземпляра Описатель иконки Описатель курсора Описатель фоновой кисти Описатель маленькой иконки Оконный объект — Имя класса Заголовок Стиль Положение Размер Описатель родительского окна Описатель экземпляра Рис. 2.2. Структуры, описывающие класс, окно и данные приложения для Windows Оконная процедура занимается обработкой всех поступающих приложению сообщений. Все сообщения имеют один и тот же формат. На рис. 2.3 он показан на примере сообщения WM_COMMAND. Код сообщения уникален для каждого вида сообщений. В одном из заголовоч- ных файлов Windows перечислены коды всех сообщений, которые Windows мо- жет отправить приложению. Эти числовые коды представлены в символьном виде, например WMCOMM AND. Идентификатор Код | получателя сообщения I Зависящие от вида сообщения аргументы ; HWND 3! WM_COMMAND V WParam 1 г " " 1 Iparam itemHandle NotifyCode ItemID Старшее слово Младшее слово Рис. 2.3. Формат типичного сообщения Windows
Анатомия простой программы для Windows 1|||ММ1^Н 39 СОВЕТ _ Каждое адресованное окну сообщение проходит через оконную процедуру при- ложения. Важные сообщения обрабатываются самой процедурой, а остальные передаются системе для обработки по умолчанию. Если сообщение не обработано и не передано системе, возможны странные эффекты. Например, некоторая последовательность сообщений заставляет Windows нарисовать полосу заголовка для окна приложения. Если не передать эти сообщения для обработки по умолчанию, то полоса заголовка вообще не по- явится. Такое окно вряд ли понравится пользователю. Помимо кода, в каждом сообщении есть два параметра, которые называются wParam и IParam. Эти имена восходят к ранним версиям Windows, когда первый параметр был 16-разрядным числом типа WORD, а второй - 32-разрядным чис- лом типа LONG. Сейчас оба параметра 32-разрядные, но изменять документацию очень трудоемко. Поэтому остались старые имена. Интерпретация этих параметров зависит от кода сообщения. Ответственность за правильную интерпретацию ложится на оконную процедуру. Выяснить смысл параметров можно несколькими способами. Первый - прочитать документацию. Так, из рис. 2.3 видно, что параметр wParam на самом деле состоит из двух частей. В старшем слове находится код извещения, который показывает, какая операция выполнена над элементом управления. Например, поле ввода посылает извеще- ние EDIT_CHANGE при модификации его содержимого. В младшем слове wParam находится числовой идентификатор элемента управления (скажем, кноп- ки), который отправил сообщение WM_COMMAND оконной процедуре. Сущест- вует несколько макросов для выделения этих частей из параметра wParam, в частнос- ти HIWORD и LOWORD. Мы покажем, как применять эти макросы, в примере ниже. Прежде чем приступать к написанию первой программы для Windows, надо сказать еще об одной вещи. Многие вызовы Win32 API требуют в качестве аргу- мента описатель экземпляра. На рис. 2.4 приведена простая иллюстрация этого понятия. Приложению необходим какой-то способ идентифицировать объект, пред- ставляющий загруженную в память программу. Можно было бы использовать для этой цели полный путь к исполняемому файлу, но это слишком накладно. Ведь всякий раз, как программа захочет сослаться на собственный экземпляр в памяти, пришлось бы передавать одну и ту же строку. Поэтому в Win32 API при- меняется целочисленный идентификатор, который и называется описателем эк- земпляра. Его создает Windows и передает программе на стадии инициализации. На рис. 2.4 представлена и другая важная концепция. Каждая загруженная в память программа Windows состоит из двух частей. Первая - это, конечно, ее двоичный код. А вторая - набор двоичных ресурсов: меню, диалогов, инструмен- тальных панелей, иконок, растровых изображений и строк. Все они находятся в таблице ресурсов. Во время работы приложение может получить доступ к своим ресурсам и использовать их для взаимодействия с пользователями.
40 Illi Типичные программы для Pocket PC. Общая логическая структура программ для Windows Как следует из предыдущей и настоящей глав, функция WinMain в любой программе должна выполнять следующие действия. 1. Зарегистрировать новый оконный класс, заполнив некоторую структуру данных. 2. Создать окно на основе зарегистрированного класса, заполнив другую структуру. 3. Показать окно пользователю. 4. Выбирать сообщения из очереди. 5. Передавать сообщения оконной процедуре для обработки. Описатель экземпляра 1 ► Кеш объект ртжжяжиивл Уникальный целочисленный 1 идентификатор 1 Двоичные ресурсы Рис. 2.4. Структура программы для Windows Именно так устроена функция WinMain в следующем ниже листинге 2.1. Функция WndProc также организована определенным образом, а именно. 1) имеется предложение switch, осуществляющее ветвление по коду сообщения; 2) для каждого сообщения, представляющего интерес для программы: □ из сообщения извлекаются параметры; □ выполняется некоторое действие; 3) необработанные сообщения передаются Windows для обработки по умол- чанию. В листинге 2.2 приведена полная реализация функции WndProc согласно опи- санной схеме. Типичная программа для Windows В этой книге принят единый подход: сначала приводится полный исходный текст программы, а затем производится его построчный анализ. Таким образом мы можем показать общую организацию кода и его составных частей, а впоследст- вии пояснить назначение каждой строки. СОВЕТ Ради экономии места мы обычно опускаем объявления переменных стандартных типов, например целых или с плавающей точкой. Но в исходных текстах на сопро- водительном сайте (http://www.osborne.com) они присутствуют
Общая структура программ для Windows 1И1МШ I 41 Полный текст функции WinMain В листинге 2.1 приведен полный текст функции WinMain для программы, ис- полняемой на настольном ПК. Листинг 2.1. Полный текст функции WinMain * * File: WinMain.с * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ***********************************************/ ♦include <windows.h> ♦include <windowsx.h> BOOL CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM IParam); int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR IpCmdLine, int nCmdShow) ( HWND hwnd; HICON hicon ; HCURSOR hcursor ; HBRUSH hbrush ; WNDCLASSEX wclass ; MSG msg ; hicon = Loadicon( NULL , IDI_APPLICATION ) ; hcursor = LoadCursor( NULL, IDC_ARROW ) ; hbrush = GetStockObject( WHITE_BRUSH ) ; wclass.cbSize = sizeof(WNDCLASSEX) ; wclass.style = CS_HREDRAW I CS_VREDRAW ; wclass.lpfnWndProc = (WNDPROC)WinProc ; wclass.hlnstance = hlnstance ; wclass.hicon = hicon ; wclass.hCursor = hcursor ; wclass.hbrBackground = hbrush ; wclass.IpszMenuName = NULL ; wclass.IpszClassName = "HelloWorld Class" ; wclass.hlconSm = hicon ; wclass.cbClsExtra = 0 ; wclass.cbWndExtra = 0 ; RegisterClassEx ( Swclass ) hwnd = CreateWindowEx( 0 , "HelloWorld Class" , "HelloWorld Program" ,
Типичные программы для Pocket PC 42 ws_overlapped , о, о , 288,375, NULL , NULL , hlnstance , NULL ) ; ShowWindow( hwnd , nCmdShow ) ; UpdateWindow( hwnd ) ; while ( GetMessage( imsg, NULL , 0 , 0 )) ( TranslateMessage( imsg ) ; DispatchMessage( imsg ) ; } return msg.wParam ; } Эта программа следует общей схеме, типичной для всех Windows-программ. Сначала идут определение, создание и отображение главного окна. Затем начина- ется цикл выборки сообщений с передачей их оконной процедуре для обработки. Построчный анализ функции WinMain Теперь можно заняться подробным анализом. Мы не будем останавливаться на таких очевидных элементах, как объявление переменных часто используемых типов или фигурные скобки, отмечающие начало и конец блока. #include <windows.h> #include <windowsx.h> Эти два файла включают множество других заголовочных файлов, в которых содержатся объявления типов данных и функций. Так, windows.h включает, в част- ности, файл winuser.h. Его имя говорит о том, что в нем содержатся объявления, относящиеся к компоненту USER. BOOL CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM IParam); В этой строке объявляется сигнатура оконной процедуры, которая должна быть в каждом приложении. Сигнатура включает тип возвращаемого значения, имя функции и список аргументов. Кроме того, в нем есть часть, характерная именно для программирования в Windows. Это слово CALLBACK, которое час- то называют уточнением (adornment). Уточнения несут информацию, необхо- димую компилятору для правильной генерации кода. В данном случае слово CALLBACK определяет порядок помещения в стек аргументов, передаваемых функции. Сигнатура функции WinProc допускает некоторую гибкость. Имена самой функции и ее аргументов могут отличаться от указанных в сигнатуре. Но тип воз- вращаемого значения, набор уточнений и типы аргументов должны в точности совпадать с объявленными. Это необходимо для того, чтобы размеры и порядок аргументов в стеке соответствовали тем, чего ожидает Windows. В противном слу- чае могут произойти неприятности. В самом деле, Windows помесг ’ >а-
Типичная программа для Windows 1П1П 43 вильные значения, а оконная процедура начнет интерпретировать их совсем по- другому. В результате сообщения будут обрабатываться некорректно, и поведе- ние программы станет непредсказуемым. int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR IpCmdLine, int nCmdShow) Этот заголовок функции точно соответствует сигнатуре WinMain, требуемой Windows. Здесь мы встречаем еще одно уточнение - WINAPI. Его смысл точно такой же, как у описанного выше уточнения CALLBACK. Первый аргумент функ- ции, hlnstance, - это уникальный числовой идентификатор загруженной в память программы. Во втором аргументе, hPrevInstance, Windows всегда передает 0. Он присутствует только для совместимости с предыдущими версиями, и для вновь разрабатываемых программ совершенно бесполезен. Однако если его опустить, то многие существующие программы перестали бы работать. Следующий аргумент IpCmdLine содержит указатель на строку, содержащую параметры, переданные программе при запуске. Для большинства программ эта строка пуста. И после- дний аргумент nCmdShow определяет начальное состояние окна. Почти всегда он равен SW_SHOW, то есть окно должно быть видимо. В этом заголовке меняться могут только имена аргументов. Если вы измените что-то еще, программа просто не откомпилируется или не будет работать. Совме- стно Windows и Visual C++ определяют WinMain как точку входа в программу. Если функции с таким именем и сигнатурой не будет обнаружено, то Visual C++ недвусмысленно заявит об этом. HWND HICON HCURSOR HBRUSH WNDCLASSEX MSG hwnd; hicon ; hcursor hbrush ; wclass ; msg ; Эти переменные служат для хранения различной информации, используемой в разных частях программы. Типы данных специфичны для Windows. Например, тип HWND говорит, что в переменной хранится описатель окна. В терминологии, принятой в Windows, описателем называется уникальное целое число, используе- мое для доступа к внутренней структуре данных. Если приложение желает как-то манипулировать этой структурой, то должно передать соответствующей функции ее описатель. Напрямую программа никогда не обращается к внутренним дан- ным, только через описатель и функцию API. Пресекая все попытки несанкцио- нированного изменения внутренних данных, Windows может гарантировать це- лостность операционной системы. Интерес представляют еще некоторые типы данных, встречающиеся в объяв- лениях. Структура WNDCLASSEX необходима для определения оконного клас- са приложения. В переменную типа MSG помещаются сообщения, извлекаемые из очереди основного потока. hicon = Loadicon( NULL , IDI_APPLICATION ) ; hcursor = LoadCursor( NULL, IDC_ARROW ) ;
44 Illi Типичные программы для Pocket PC Эти две строки нужны для получения и запоминания описателей некоторых ресурсов приложения. У каждой функции два аргумента. Первый определяет за- груженный объект, владеющий ресурсом, обычно в этом качестве выступает опи- сатель экземпляра. Если же значение аргумента равно NULL, то владельцем ре- сурса считается Windows. ПРЕДОСТЕРЕЖЕНИЕ В приложениях для Windows СЕ значением этого аргумента может быть только NULL Если вместо этого передать описатель экземпляра, то функция вернет NULL. Если затем возвращенное значение передать функции RegisterClassExfinn регистрации оконного класса, то она вернет ошибку, приложение не сможет нор- мально завершить инициализацию, и его окно так и не появится на экране. В качестве второго аргумента обе функции ожидают числовой идентифика- тор ресурса. В данном случае мы указали предопределенные идентификаторы си- стемных ресурсов. Так, Loadicon передан идентификатор IDI_APPLICATION, который соответствует стандартной системной иконке приложения, а значение IDC_ARROW, переданное LoadCursor, представляет системный курсор. hbrush = GetStockObject( WHITE_BRUSH ) ; Интерфейс GDI предоставляет ряд готовых объектов, представляющих раз- личные инструменты рисования. Здесь мы запрашиваем белую кисть. Есть и еще несколько готовых кистей, например, DKGRAY_BRUSH и LTGRAY_BRUSH. Функция GetStockObject предполагает, что владельцем ресурса является GDI, а ресурс - это тот или иной инструмент рисования. wclass.cbSize = sizeof(WNDCLASSEX) ; В этой строке начинается инициализация структуры, описывающей оконный класс. Windows требует, чтобы программа явно заполнила все поля структуры WNDCLASSEX. При этом поле cbSize позволяет Windows определить версию ис- пользованной структуры данных. wclass.style = CS_HREDRAW I CS_VREDRAW ; Эти константы, указанные в поле style, говорят, что окно данного класса долж- но автоматически перерисовываться при изменении положения или размера. wclass.lpfnWndProc = (WNDPROC)WinProc ; wclass.hlnstance = hlnstance ; wclass.hlcon = hicon ; wclass.hCursor = hcursor ; wclass.hbrBackground = hbrush ; wclass.IpszMenuName = NULL ; Эти поля определяют различные характеристики окон данного класса. Для всех окон одного класса сообщения будут передаваться одной и той же оконной процедуре WinProc, адрес которой записывается в поле lpfnWndProc. Если вы из- мените имя процедуры, то в первой строке нужно будет отразить это изменение. В поле hbrBackground помещается описатель кисти, которой GDI будс' ' " '
Типичная программа для Windows 1ННП 45 вать фон окна. В поле IpszMenuName обычно заносится NULL. Если записать в него имя меню, то система будет пользоваться одним и тем же меню для всех окон данного класса. Как правило, инициализация меню производиться динами- чески в оконной процедуре. wclass.IpszClassName = "HelloWorld Class" ; Здесь задается имя оконного класса приложения. Windows ведет список окон одного класса. В Win32 API есть функции для обхода всех окон указанного клас- са, которые ожидают получить имя класса в качестве аргумента. wclass.hlconSm = hicon ; wclass.cbClsExtra = 0 ; wclass.cbWndExtra = 0 ; При сворачивании приложения Windows отображает иконку, описатель кото- рой указан в поле hlconSm. СОВЕТ Поскольку Windows СЕ не поддерживает сворачивания окон так, как это делают версии для настольных ПК, то в варианте этой программы для Windows СЕ поле hlconSm не инициализируется. RegisterClassEx ( &wclass ) ; После заполнения всех полей структура WNDCLASSEX передается функ- ции RegisterClassEx. В этот момент Windows копирует значения полей во внут- реннюю структуру. Последующие ссылки на оконный класс производятся по его имени. hwnd = CreateWindowEx( 0 , "HelloWorld Class" , "HelloWorld Program" , WS_OVERLAPPED , 0, 0 , 288,375, NULL , NULL , hlnstance , NULL ) ; Итак, оконный класс зарегистрирован, и можно создавать окно этого класса. Аргументами функции служат имя оконного класса и ряд параметров, описываю- щих конкретное окно. В дополнение к этим параметрам окно наследует все харак- теристики, заданные в его классе, и самая важная из них - это оконная процедура, которая будет обрабатывать все сообщения. Первый аргумент содержит комбинацию констант, описывающих расширен- ный стиль. Они позволяют сказать, в частности, что данное окно должно распола- гаться поверх других, а также задать другие особенности поведения. Если в стиле задана константа WS_OVERLAPPED, то показываются строка заголовка и рамка окна. Есть и другие стили, но к программам для Windows СЕ они не относятся. Для каждого окна следует задать начальную точку и размер. Первая пара аргу- ментов (0,0) определяет положение левого верхнего угла окна относительно лево- го верхнего угла экрана. Далее задаются ширина (288) и высота (375) окна. При таких размерах окно заполнит почти весь экран КПК. Остальные аргументы не- обходимы для создания дочерних окон и задания дополнительных начальных
46 Illi Типичные программы для Pocket PC данных. О том, как они используются, мы еще поговорим при рассмотрении окон- ной процедуры. Получив всю эту информацию, Windows сохраняет ее в системной структуре данных и возвращает ее описатель. Showwindow( hwnd , nCmdShow ) ; UpdateWindow( hwnd ) ; Эти функции выводят окно на экран. Получив описатель конкретного окна, Windows может отобразить его, пользуясь данными из ассоциированной с этим окном внутренней структуры. Так, зная начальную точку и размер, Windows по- мещает окно в определенное место на экране. Если программа не вызовет эти функ- ции, то окно не появится. В результате выполнения функции ShowWindow на экране рисуются строка заголовка и рамка окна. Затем Windows создает сообщение WM_PAINT, предла- гая заполнить клиентскую область. Функция UpdateWindow помещает это сооб- щение в начало очереди сообщений основного потока и вызывает оконную проце- дуру, что приводит к первому акту рисования в клиентской области. while ( GetMessage( Smsg, NULL , 0 , 0 )) { TranslateMessage) &msg ) ; DispatchMessage( &msg ) ; ) После того как окно отображено и его клиентская область заполнена, про- грамма входит в цикл выборки и обработки сообщений из очереди основного по- тока. В заголовке цикла вызывается функция GetMessage, которая делает две вещи. Она копирует сообщение, находящееся в начале очереди, в свой аргумент msg. Если это оказалось сообщение WM_QUIT, то функция возвращает значение FALSE, в результате чего цикл завершается. Если же возвращено значение TRUE, то продолжается исполнение цикла. Остальные аргументы позволяют просмат- ривать диапазон сообщений, но для нас они интереса не представляют. Если функция GetMessage обнаруживает, что очередь основного потока пус- та, то этот поток приостанавливается, и планировщик в ядре передает управление другому потоку. Основной поток приложения не возобновляет работу, пока в оче- редь не поступит новое сообщение. В этот момент он ставится в очередь готовых к выполнению потоков, ожидая пока планировщик не выделит ему процессор. Функция TranslateMessage в теле цикла смотрит, соответствует ли сообщение нажатию клавиши, и если это так, то помещает в начало очереди сообщение WM_CHAR. При следующем вызове GetMessage извлечет это сообщение, и окон- ная процедура сможет обработать его способом, не зависящим от аппаратных осо- бенностей клавиатуры. Вслед за TranslateMessage вызывается функция DispatchMessage, которая передает сообщение оконной процедуре, зарегистриро- ванной в оконном классе приложения. return msg.wParam ; По выходе из цикла переменная msg содержит сообщение WM_QUIT. Его поле wParam содержит значение аргумента, переданного функш"‘ ' 1П ' поме
Типичная программа для Windows 1ННВП1 47 стила сообщение WM_QUIT в очередь. Именно оно и возвращается Windows в качестве кода завершения программы. Обычно это значение равно 0, что свиде- тельствует о нормальном завершении. Полный текст функции WinProc В листинге 2.2 приведен полный текст функции WinProc для простой про- граммы, исполняемой на настольном ПК. Листинг 2.2. Полный текст функции WinProc /•к***************************************'»****** * File: WinProc.с * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ ♦include <windows.h> ♦include <windowsx.h> BOOL CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM IParam ) { HINSTANCE Instance ; int ilD ; HDC DeviceContext ; PAINTSTRUCT Paint ; RECT Rectangle ; HBRUSH Brush ; switch (message) { case WM_CREATE: Instance = GetWindowInstance(hWnd) ; SetWindowPos(hWnd, NULL,0,0,0,0,SWP_NOSIZE | SWP_NOZORDER) ; CreateWindowEx(0,"BUTTON","OK",WS_CHILD|WS_VISIBLEIBS_PUSHBUTTON, 10,10,40,40, hWnd, (HMENU)IDOK, Instance,NULL) ; return TRUE ; case WM_COMMAND: ilD = LOWORD (wParam) ; switch) ilD ) { case IDOK: PosrQuitMessage(0) ; break ; } return FALSE ;
48 III Типичные программы для Pocket PC case WM_PAINT: DeviceContext = BeginPaint(hWnd, SPaint) ; GetClientRect(hWnd,SRectangle) ; Brush = (HBRUSH)GetStockObject(WHITE_BRUSH) ; FillRect(DeviceContext,SRectangle, Brush) ; EndPaint(hWnd,SPaint) ; return FALSE ; case WM_MOVE: SetWindowPos(hWnd,NULL,0,0,0,0,SWP_NOSIZE I SWP_NOZORDER) ; return FALSE ; case WM_CTLCOLORSTATIC : return ((DWORD) GetStockObject(WHITE_BRUSH)) ; ) return DefWindowProc(hWnd,message,wParam,IParam) ; 1 Этот код следует общей схеме написания оконной процедуры для всех Windows-программ. Предложение switch выбирает нужный обработчик сообще- ния, если приложение в нем заинтересовано. В противном случае сообщение пере- дается функции DefWindowProc для обработки по умолчанию. Построчный анализ функции WinProc По мере возможности мы будем анализировать сразу несколько строк. Выше необходимо было остановиться на ряде существенных концепций и определений. Повторяться мы не станем. BOOL CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM IParam ) Напомним, что у оконной процедуры должна быть именно такая сигнатура. Когда функция WinMain вызывает DispatchMessage, та разбирает сообщение и передает его поля в виде аргументов оконной процедуре. Аргумент hWnd опреде- ляет структуру данных, соответствующую окну, которое породило сообщение. В самом сообщении есть числовой код, определяющий его тип. Остальные аргу- менты содержат дополнительную информацию о контексте сообщения. Обработ- чики используют эту информацию, чтобы приложение могло должным образом отреагировать. HDC DeviceContext ; PAINTSTRUCT Paint ; RECT Rectangle ; Из всех объявленных переменных только эти заслуживают рассмотрения. Пе- ременная DeviceContext типа HDC содержит уникальный числовой идентифи- катор контекста устройства. Выше мы говорили, что контекст устройства - это набор инструментов рисования для конкретного устройства отображения. Иден- тификатор позволяет Windows управлять доступом из программы к системным
Типичная программа для Windows !1Н1М 49 структурам данных, в которых хранятся инструменты рисования. Структура PAINTSTRUCT содержит подробную информацию о части клиентской области, нуждающейся в перерисовке. В структуре RECT хранятся координаты левой, верхней, правой и нижней сторон произвольного прямоугольника. В совокупнос- ти эти три переменные играют ключевую роль при обновлении клиентской облас- ти окна приложения. switch (message) В этой строке начинается ветвление по коду сообщения с целью передать его нужному обработчику. case WM_CREATE: Instance = GetWindowInstance(hWnd) ; Это сообщение попало в очередь, когда приложение вызвало функцию CreateWindowEx. Его цель - дать приложению возможность выполнить инициа- лизацию. Инициализация нашего приложения состоит из двух основных шагов: фикси- ровать начальное положение окна и создать кнопку внутри клиентской области. Для создания кнопки необходим описатель экземпляра родительского прило- жения. Поэтому обработчик сначала получает этот описатель с помощью макроса GetWindowInstance, который извлекает его из внутренней структуры данных, зная описатель окна. Макрос становится доступен, если включить заголовочный файл windowsx.h. SetWindowPos(hWnd,NULL,О,О,О,О,SWP_NOSIZE | SWP_NOZORDER) ; Левый верхний угол окна помещается в точку с координатами 0,0, то есть в левый верхний угол экрана. Функция SetWindowPos на самом деле позволяет выполнить три разные операции: установить положение окна, его размер, а также z-порядок окна. Z-порядок определяет положение окна вдоль воображаемой оси аппликат, то есть то, какие окна перекрывают данное, а какие перекрываются им. Флаги, заданные в последнем аргументе, говорят, что аргументы, определяющие размер и z-порядок, следует игнорировать. Следовательно, интерес представляют1 только аргументы, задающие положение, то есть два числа, следующие за аргу- ментом NULL. Это и есть координаты левого верхнего угла окна относительно левого верхнего угла экрана. CreateWindowEx(0,"BUTTON","OK",WS_CHILD|WS_VISIBLEIBS_PUSHBUTTON, 10,10,40,40, hWnd, (HMENU)IDOK, Instance, NULL) ,- На рис. 2.1 показано, что графический интерфейс программы содержит одну кнопку. Когда пользователь касается ее стилосом, программа должна завершить- ся. Предложение выше создает кнопку, задает ее стиль и отображает. Второй аргумент говорит, что эта кнопка, которая в действительности яв- ляется еще одним окном, наследует предопределенные свойства от класса «BUTTON», который предоставляется Win32 API. Вслед за именем класса идет строка, содержащая надпись «ОК», которая должна быть нарисована на кнопке. Затем мы видим набор стилей. Конкретно данное окно является дочерним, види-
50 Типичные программы для Pocket PC III! мым и обладает предопределенным поведением нажимаемой кнопки. Числа после стилей задают положение и размеры кнопки. Координаты отсчитываются от ле- вого верхнего угла клиентской области. В Windows они называются координа- тами клиентской области. Такую интерпретацию координат обеспечивает стиль WSCHILD. Поскольку данный элемент управления является дочерним окном, то следую- щие два аргумента определяют отношения родства. hWnd - это идентификатор родительского окна, в нашем случае это главное окно приложения. Вслед за ним идет числовой идентификатор самого окна кнопки IDOK. В файле windows.h эта символическая константа определена с помощью директивы #define. Приведение к типу HMENU необходимо, чтобы компилятор не «ругался» на несоответствие типов. К сожалению, в Win32 API решено возложить на этот аргумент двойную нагрузку: идентифицировать дочернее окно или служить описателем меню. Конк- ретную интерпретацию в данном случае определяет все тот же стиль WS_CHILD, который говорит, что мы имеем дело с дочерним окном. Аргумент Instance описы- вает экземпляр загруженной программы. В последнем аргументе программа мо- жет задать указатель на область памяти, содержащую дополнительные данные. У нас их нет, поэтому мы передали значение NULL. return TRUE ; Когда программа вызвала функцию CreateWindow для создания главного окна приложения, оконная процедура получила сообщение WM_CREATE. Но Windows должна проделать кое-какую дополнительную работу. Возвращая TRUE, этот обработчик предлагает Windows заняться этим. Если бы обработчик вернул FALSE, то главное окно так и не появилось бы, поскольку его создание не завершено. case WM_COMMAND: ilD = LOWORD(wParam) ; switch) ilD ) ( case IDOK: PostQuitMessage(0) ; break ; } return FALSE ; Когда пользователь коснется кнопки, в очередь будет помещено сообщение WM_COMMAND. Но такие сообщения генерируют все кнопки, входящие в со- став графического интерфейса приложения. Поэтому программа должна как-то идентифицировать источник. Для этого обработчик анализирует параметры сооб- щения. Согласно документации, младшее слово параметра wParam содержит це- лочисленный идентификатор дочернего элемента, от которого пришло сообще- ние. В Win32 API есть макрос LOWORD, который извлекает этот идентификатор и помещает его в переменную. Разобравшись с источником, мы выполняем ветв- ление, передавая управление обработчику команд именно от данного источника, то есть кнопки с идентификатором IDOK.
Типичная программа для Windows Ullin 51 В ответ на нажатие этой кнопки вызывается функция PostQuitMessage, кото- рая помещает в очередь сообщение WM_QUIT. Аргумент этой функции - код за- вершения программы (обычно 0), который станет значением параметра wParam в сообщении WMQUIT. Когда это сообщение достигнет начала очереди, про- изойдет выход из цикла, как мы уже объясняли выше. В отличие от WM_CREATE, обработчик сообщения WMCOMMAND воз- вращает FALSE. Это говорит Windows о том, что дальнейшая обработка данного сообщения не требуется. В противном случае Windows могла бы продолжить его обработку, что привело бы к нежелательным эффектам. case WM_PAINT: DeviceContext = BeginPaint(hWnd,SPaint) ; GetClientRect(hWnd, SRectangle) ; Brush = (HBRUSH)GetStockObject(WHITE_BRUSH) ; FillRect(DeviceContext,SRectangle,Brush) ; EndPaint(hWnd,SPaint) ; return FALSE ; Ответственность за перерисовывание клиентской области возлагается на саму программу. Обработчик сообщения WM_PAINT выполняет операции рисо- вания, представляя результаты пользователю. Рисование должно следовать опре- деленному протоколу, включающему две функции: BeginPaint и EndPaint. Цель BeginPaint - получить описатель контекста устройства с инструментами рисова- ния для окна и поднять флаг, говорящий о том, что клиентская область успешно обновлена. По завершении операций обновления функция EndPaint освобождает контекст устройства, чтобы им могли воспользоваться другие приложения. Струк- тура PAINTSTRUCT, заполняемая функцией BeginPaint, уже содержит описатель контекста устройства, поэтому отдельно передавать его функции EndPaint нет необходимости. Компонент GDI хранит ограниченное число контекстов устройств, поэтому очень важно вовремя освобождать захваченный контекст. з ПРЕДОСТЕРЕЖЕНИЕ_______________________________________________ 9 Если обработчик сообщения WM_PAINT не вызовет вначале функцию BeginPaint, Е то Windows будет думать, что клиентская область по-прежнему нуждается в пере- рисовке. В результате приложение войдет в бесконечный цикл. Визуально это будет проявляться в том, что программа зависнет, а окно будет мигать. Если не вызывать EndPaint, произойдет другая неприятность. Рано или поздно все имею- щиеся контексты устройств будут захвачены, и это приведет к неправильному рисованию как в некорректном, так и во всех остальных приложениях. Типичное проявление этой проблемы состоит в том, что программы начинают рисовать исключительно черным пером, даже если явно был запрошен другой цвет. В нашем примере обработчик сообщения WM_PAINT решает очень простую задачу — закрашивает клиентскую область белой кистью. Для этого обработчик вызывает функцию GetClientRect, которая возвращает структуру Rectangle, ини-
52 Типичные программы для Pocket PC Illi диализированную так, что она совпадает со всей клиентской областью. Затем с помощью функции GetStockObject обработчик получает белую кисть. В заклю- чение кисть и закрашиваемая область передаются в качестве аргументов функции FillRect, которая и выполняет закрашивание. В других приложениях после закра- шивания могут выполняться какие-то операции рисования поверх фона. После закрашивания цвет клиентской области будет отличаться от цвета по- лосы меню. В противном случае они бы сливались. Эстетически приятнее, когда клиентская область и полоса меню воспринимаются как две различные части окна. Кроме того, исследования дизайна Web-сайтов показали, что белый цвет клиентской области наиболее комфортен для зрительного восприятия; пользова- тель может дольше смотреть на нее, не утомляясь. case WM_MOVE: SetWindowPos(hWnd,NULL,О,О,О,О,SWP_NOSIZE I SWP_NOZORDER) ; return FALSE ; Это сообщение оконная процедура получает, когда пользователь перемещает окно, буксируя мышью полосу меню. Наш обработчик восстанавливает окно в на- чальное положение - в левом верхнем углу экрана. Для большинства программ, предназначенных для настольного ПК, такое поведение необязательно. А в случае Windows СЕ оно навязывается совершенно иным способом, о котором мы погово- рим в следующей главе. Кстати говоря, буксировка окна по экрану КПК эстетически неприятна. Из-за малого размера экрана пользователю затруднительно работать с частично пере- крытыми окнами. С точки зрения удобства приложение, которое заставляет пользователя сосредоточиться на единственном окне, гораздо предпочтительнее. case WM_CTLCOLORSTATIC : return ((DWORD) GetStockObject(WHITE_BRUSH)) ; Это сообщение Windows посылает родительскому окну, перед тем как рисо- вать метку. Оно позволяет оконной процедуре закрасить фон в области метки тем же цветом, что и для всей клиентской области. ПРЕДОСТЕРЕЖЕНИЕ Если это сообщение обрабатывается иным способом, то фоновый цвет меток бу- дет отличаться от цвета клиентской области, поэтому метки будут казаться зак- люченными в прямоугольник. Поскольку обработчик сообщения WM_PAINT закрашивал клиентскую об- ласть белой кистью, то так же должен поступить и обработчик этого сообщения. Почему-то Windows ожидает, что этот обработчик вернет описатель той кис- ти, которой должен быть закрашен фон метки, хотя обычно обработчики возвра- щают булевское значение TRUE или FALSE. Если поступить так и в этом случае, то фон вообще не закрасится. Отметим, что описатель кисти предварительно при- водится к типу DWORD. Если забыть про это, то никакого закрашивания также не произойдет. return DefWindowProc(hwnd,message,wParam,IParam) ,
Преобразование программы ииип 53 Любая операция с окном приложения приводит к отправке ему сообщения. Так, Windows посылает серию сообщений, извещающих о необходимости рисова- ния в неклиентских областях, например в полосе заголовка. Приложение обязано передать эти сообщения Windows для обработки по умолчанию. Наша оконная процедура устроена так, что если некоторое сообщение ей не обрабатывается, то управление попадает приведенному выше предложению, в котором вызывается функция DefWindowProc. Эта функция и выполняет обработку по умолчанию. Возвращаемое ей значение говорит Windows, что сообщение успешно обработано. ПРЕДОСТЕРЕЖЕНИЕ Если не передать необработанные сообщения функции DefWindowProc, то может произойти одна из двух неприятностей. Возможно, клиентская область будет на- рисована, но без рамки и полосы заголовка. Или окно вообще не появится, но программа будет исполняться; в таком случае завершить его можно лишь с по- мощью диспетчера задач (Task Manager). Преобразование программы для исполнения на платформе Windows СЕ В этом разделе мы опишем, какие изменения необходимо провести, чтобы эта программа удовлетворяла требованиям Windows СЕ, описанным выше. Ниже приводится перечень изменений в каждой из двух основных функций. Модификации функции WinMain 1. Объявить глобальную переменную Instance для хранения описателя экземп- ляра программы. 2. Преобразовать аргумент IpCmdLine функции WinMain к платформенно- независимому типу LPTSTR. 3. Заменить структуру WNDCLASSEX структурой WNDCLASS. 4. Вставить после объявлений переменных строку, которая сохранит описа- тель экземпляра hlnstance в глобальной переменной Instance. 5. Вместо идентификаторов системных ресурсов иконки и курсора подста- вить нули. 6. Заключить строковый литерал, содержащий имя класса, в макрос_TEXT. 7. Убрать предложения инициализации полей cbSize и hlconSm. 8. Заключить строковые литералы, содержащие аргументы функции CreateWindowEx, в макрос__TEXT. Если не выполнить какой-либо из этих шагов, то либо программа не откомпи- лируется, либо произойдет ошибка на этапе инициализации. Вообще говоря, для успешной компиляции необходимы шаги, связанные с преобразованием типов и заменой функций. Остальные изменения нужны для того, чтобы программа пра- вильно инициализировалась и отобразила окно.
54 Illi Типичные программы для Pocket PC ПРИМЕЧАНИЕ Тот факт, что программа успешно откомпилировала^, еще не гарантирует пра- вильной работы. Хуже того - если программа не заработает, то не будет почти никакой информации о причине ошибки. Чтобы отыскать ее, придется заняться дистанционной отладкой. Но каркас, который мы опишем в следующей главе, делает многое для того, чтобы такого рода проблемы не возникали. Обсуждение модификаций WinMain Некоторые из описанных выше модификаций заслуживают более подробного обсуждения. В варианте для настольного ПК заголовочный файл windowsx.h со- держит макрос для извлечения описателя экземпляра из системных таблиц. Для Windows СЕ такого макроса и соответствующих констант нет. Это удивительно, поскольку в Win32 API существует довольно много функций, нуждающихся в описателе экземпляра. Но ничего не поделаешь, приходится объявлять глобаль- ную переменную для его сохранения. Делать это нужно сразу при входе в функ- цию WinMain. В Windows СЕ не поддерживается структура WNDCLASSEX, которая содер- жит два дополнительных поля: размер самой структуры и описатель иконки, ото- бражаемой, когда программа свернута. Поскольку Windows СЕ не отображает иконки свернутых программ, то эта информация не нужна. Кроме того, функции, которым передаются структуры WNDCLASS и WNDCLASSEX, различаются. В версии программы для настольного ПК используются идентификаторы ресурсов IDI_APPLICATION (для иконки) и IDC_ARROW (для курсора). Но в Visual C++ для Windows СЕ эти константы не поддерживаются. Единственный способ получить стандартные ресурсы - подставить вместо них 0. Остальные шаги связаны с переходом от ASCII-строк к платформенно-неза- висимым. Мы уже обсуждали этот вопрос выше. Аннотированный исходный текст модифицированной функции WinMain Мы уже детально обсудили код WinMain выше, поэтому сейчас применим не- сколько иной подход. В листинг 2.3 включены комментарии, показывающие, где применены описанные в предыдущем разделе шаги. Листинг 2.3. Аннотированный исходный текст модифицированной функции WinMain X jJ* * /*★★★*★***★★★★★*★★★★★★★★★★*★*********•+*******•*** * File: WinMain.с * * copyright, SWA Engineering, Inc., 2001 * All rights reserved.
Преобразование программы IIIUMM 55 ***********************************************/ ♦include <windows.h> ♦include <windowsx.h> BOOL CALLBACK WinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam) ; HINSTANCE Instance ; // Шаг 1 int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR IpCmdLine, int nCmdShow) // Шаг 2 { HWND hwnd; HICON hicon ; HCURSOR hcursor ; HBRUSH hbrush ; WNDCLASS wclass ; // Шаг 3 MSG msg ; Instance = hlnstance ; // Шаг 4 hicon = Loadicon( NULL , 0 ) ; // Шаг 5 hcursor = LoadCursor ( NULL, 0 ) ; // Шаг 5 hbrush = GetStockObject( WHITE_BRUSH ) ; wclass.style = CS_HREDRAW | CS_VREDRAW ; wclass.IpfnWndProc = (WNDPROC)WinProc ; wclass.hlnstance = hlnstance ; wclass.hicon = hicon ; wclass.hCursor = hcursor ; wclass.hbrBackground = hbrush ; wclass.IpszMenuName = NULL ; wclass.IpszClassName = __TEXT("HelloWorld Class") ; // Шаг 6 wclass.cbClsExtra = 0 ; wclass.cbWndExtra = 0 ; // Опущено заполнение поле cbSize и hlconSm // Шаг 7 Registerclass( Swclass ) ; // Шаг 8 hwnd = CreateWindowEx( 0, __TEXT("HelloWorld Class"), // Шаг 9 __TEXT("HelloWorld Program") , WS_OVERLAPPED, 0, 0, 288,375, NULL , NULL , hlnstance , NULL ) ; Showwindow( hwnd , nCmdShow ) ; UpdateWindow) hwnd ) ; while ( GetMessage( smsg, NULL , 0 , 0 )) { TranslateMessage( Smsg ) ; DisoatchMessage( Smsg ) ;
56 Illi Типичные программы для Pocket PC return msg.wParam ; } Сопоставьте аннотированные шаги с приведенным выше описанием. Надо полагать, что вы без труда поймете смысл изменений. Модификация функции WinProc Функция WinProc также должна претерпеть некоторую модификацию для исполнения на платформе Windows СЕ. 1. Объявить ссылку на внешнюю переменную Instance, в которой хранится описатель экземпляра приложения. 2. Добавить объявление переменной Style типа LONG для хранения стиля окна. 3. В обработчике сообщения WM_CREATE установить расширенный стиль окна, запрещающий буксировку. 4. Заключить строковый литерал, передаваемый функции CreateWindowEx, в макрос____TEXT. 5. Удалить весь код из обработчика сообщения WM_MOVE. Хотя число изменений меньше, но они отражают важные различия между платформами Windows 2000 и Windows СЕ. Обсуждение модификаций WinProc Прежде всего обсуждения заслуживает способ, которым мы запрещаем букси- ровку окна в Windows СЕ. Напомним, что в версии для настольного ПК WinProc обрабатывала сообщение WM_MOVE и восстанавливала исходное положение окна в левом верхнем углу экрана. В Windows СЕ этот способ не работает просто в силу отсутствия функции SetWindowPos. Чтобы зафиксировать положение окна, наша программа поступает по-друго- му, а именно устанавливает расширенный стиль, при котором все попытки бук- сировки игнорируются. Причем сделать это надо только один раз в обработчике сообщения WM_CREATE, а не при обработке каждого сообщения WMMOVE. Style = GetWindowLong(hWnd,GWL_EXSTYLE) ; Style = Style | WS_EX_NODRAG ; SetWindowLong(hWnd,GWL_EXSTYLE, Style) ; Здесь мы сначала извлекаем из системной таблицы текущее значение стиля окна с помощью функции GetWindowLong, которой передается описатель окна и константа, определяющая интересующее нас поле. В файле windows.h есть не- сколько символических констант, соответствующих различным полям описания окна. В данном случае нам нужна константа GWL_EXSTYLE. Затем программа поднимает бит, управляющий буксировкой. На самом деле стиль - это битовый вектор, содержащий 32 бита, определяющих различные осо- бенности окна. Оператор поразрядного ИЛИ (|) с маской WS_EX_NODRAG, определенной в windows.h, устанавливает нужный бит. Затем с помощью функ- ции SetWindowLong мы заносим измененное значение стиля в " табли-
Преобразование программы 57 ипим ну. Теперь бит WS_EX_NODRAG установлен, и программа будет игнорировать все попытки пользователя отбуксировать окно в другое место. Поскольку событие WM_MOVE больше не служит основой для запрета бук- сировки, код его обработчика можно удалить. Впрочем, заложенный выше каркас еще может пригодиться на последующих этапах разработки, поэтому ветвь, соот- ветствующую этому событию, мы оставим. Аннотированный исходный текст модифицированной функции WinProc Как и в случае WinMain, мы включили в листинг 2.4 комментарии с номерами описанных выше шагов модификации. Листинг 2.4. Аннотированный исходный текст модифицированной функции WinProc * File: WinProc.с * copyright, SWA Engineering, Inc., 2001 * All rights reserved. * **»**********»******»*******»******************/ ♦include <windows.h> ♦include <windowsx.h> extern HINSTANCE Instance ; // Шаг 1 BOOL CALLBACK WinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM IParam ) { HINSTANCE Instance ; int ilD ; HDC DeviceContext ; PAINTSTRUCT Paint ; RECT Rectangle ; HBRUSH Brush ; LONG Style ; // Шаг 2 switch (message) { case WM_CREATE: Style = GetWindowLong(hWnd,GWL_EXSTYLE) ; // Шаг 3 Style = Style | WS_EX_NODRAG ; // Шаг 3 SetWindowLong(hWnd,GWL_EXSTYLE,Style) ; // Шаг 3 CreateWindowEx(0,__TEXT("BUTTON"), // Шаг 4 __TEXT("OK"), // Шаг 4 WS_CHILD|WS_VISIBLEIBS_PUSHBUTTON, 10,10,40,40,
58 Типичные программы для Pocket PC hwnd,(HMENU)IDOK,Instance,NULL) ; return TRUE ; case WM_COMMAND: ilD = LOWORD(wParam) ; switch( ilD ) { case IDOK: PostQuitMessage(0) ; break ; 1 return FALSE ; case WM_PAINT: DeviceContext = BeginPaint(hWnd,&Paint) ; GetClientRect(hwnd,iRectangle) ; Brush = (HBRUSH)GetStockObject(WHITE_BRUSH) ; FillRect(DeviceContext,iRectangle,Brush) ; EndPaint(hWnd,sPaint) ; return FALSE ; case WM_MOVE: // Шаг 5 return FALSE ; case WM_CTLCOLORSTATIC : return ((DWORD) GetStockObject(WHITE_BRUSH)) ; return DefWindowProc(hWnd,message,wParam, IParam) ; } После всех изменений Embedded Visual C++ компилирует эту программу без ошибок, и она корректно выполняется под Windows СЕ. Анализ проекта простой программы для Windows Приведенная выше программа - это типичный пример, рассматриваемый во многих книгах по программированию на платформе Windows СЕ. С точки зрения разработки реальных приложений ее проект оставляет желать много лучшего и принесет немало проблем компании, для которой составление программ для Windows СЕ является основой получения прибыли или самого существования. Если мы создаем для Windows СЕ программу, на которую возлагается важная роль в ведении бизнеса, то необходимо придерживаться следующих принципов. □ Продуктивность. Быстрое доведение приложения до рабочего состояния, чтобы минимизировать время выхода на рынок. □ Расширяемость. Простота добавления новых функций с минимальными затратами на отладку и тестирование. □ Производительность. Обеспечение высокой надежности, низкого потреб- ления памяти и быстрого времени реакции.
Анализ проекта простой программы ;!!! 59 □ Возможность повторного использования. Возможность легко создавать различные продукты. Откровенно говоря, рассмотренный выше подход к проектированию не удовлет- воряет ни одному из этих требований. И практически невозможно быстро изменить его после того, как написан большой объем кода, а сроки поставки поджимают. Выби- рать правильный способ проектирования программы нужно до этапа кодирования. При анализе текущего проекта с точки зрения продуктивности мы сталкива- емся с несколькими проблемами. Продуктивность программиста обычно выше, если у него есть возможность тестировать программу на настольном ПК еще до загрузки ее на КПК. Ведь процедура дистанционной отладки утомительна и отни- мает много времени. Как показано выше, для преобразования «настольной» вер- сии программы в «наладонную» нужно выполнить целый ряд шагов. Помножьте их число примерно на тысячу, если речь идет об оценке сложности модификации реальной программы. На такое преобразование уйдут недели, если не месяцы. Методика, которая позволила бы свести число изменений к минимуму, могла бы намного повысить продуктивность. Если переход прост, то после тестирования на настольном ПК необходимость в дистанционной отладке может вообще отпасть. С темой продуктивности связан также вопрос о генерировании элементов управления, составляющих графический интерфейс пользователя. Производи- тельность труда программиста повысится, если он сможет просто перетаскивать элементы управления. В предыдущих примерах мы создавали кнопку программ- но с помощью функции CreateWindowEx. При этом приходилось явно задавать ее положение и размеры. В случае сложного интерфейса это итеративный и очень трудоемкий процесс. Первоначально программист должен выбрать приблизи- тельные значения положения и размеров, откомпилировать программу и посмот- реть, что получилось. Если элемент оказался не там, где надо, значения коррек- тируются. Иногда приходится выполнять десять, а то и больше итераций для достижения желаемого результата. В сложном приложении процесс может рас- тянуться на несколько дней. Любая модификация также занимает время. Будь у программиста возможность перетащить элемент управления в нужное место, все оказалось бы куда проще. Расширяемость достигается за счет правильного структурирования програм- мы. Примеры, рассмотренные выше, с этой точки зрения никуда не годятся. По мере роста сложности приложения функция WinProc оказывается все более запу- танной, модифицировать и отлаживать ее становится труднее и труднее. Было бы лучше структурировать WinProc, выделив обработчик каждого сообщения в от- дельную функцию, вызываемую из предложения switch. Такую функцию и моди- фицировать проще, а для обработки нового сообщения нужно было бы лишь доба- вить еще одну функцию-обработчик и включить ее в switch. С точки зрения расширяемости, наличие глобальных переменных очень не- удобно. В поисках их объявлений и определений приходится просматривать весь исходный текст. Гораздо лучше завести класс для управления глобальными дан- ными. В нем будут содержаться все разделяемые переменные, доступ к которым производится с помощью специальных методов. В примере из этой главы функ-
60 1111 Типичные программы для Pocket PC ции WinMain и WinProc использовали общую глобальную переменную для хра- нения описателя экземпляра программы. К теме структурирования относится также вопрос о выделении параметров со- общения. Эффективно организованная программа должна автоматизировать и скрывать утомительные детали анализа сообщения. Добавление новых сообщений не должно требовать написания дополнительного низкоуровневого кода. В рас- смотренном примере код для выделения параметров сообщения WM_COMMAND явно включен в его обработчик. Но программисту не следует опускаться до такого уровня детализации при добавлении новых сообщений. Эту процедуру необходимо автоматизировать способом, допускающим повторное использование. Правильное структурирование программы позволяет повысить ее производи- тельность без дополнительных усилий со стороны программиста. Шансы, что та- кая программа окажется надежной, повышаются. Коль скоро конкретная деталь реализации была отлажена, ее можно использовать снова и снова, справедливо полагая, что функция будет работать правильно при любом вызове. Удачная структура может полностью исключить утечки памяти, характерные для про- грамм на языках С и C++, поскольку управление памятью становится более жест- ким. Память выделяется, когда необходимо, и освобождается, когда перестает быть нужной. При эффективной структуре остается небольшое число повторно используемых функций, что сокращает потребление памяти и заметно повышает производительность, несмотря даже на дополнительные накладные расходы, свя- занные с вызовом функций. Отсутствие структуры заставляет программиста копировать куски кода, вместо того чтобы создать функцию с параметрами. И в результате программа становится все больше и больше. Управление памятью оказывается неэффективным, происходят утечки, а в условиях ограниченной па- мяти КПК это серьезная проблема. Чем больше программа, тем больше она зани- мает памяти и тем медленнее реагирует на действия пользователя. Применение класса для управления глобальными данными позволяет сущест- венно повысить надежность программы. Если разделяемые данные предъявляют особые требования к памяти, то их можно удовлетворить путем разумного коди- рования методов доступа. Если приложение многопоточное, то методы доступа позволяют синхронизировать доступ к глобальным данным. А тот подход, кото- рый мы видели в примерах выше, подразумевает синхронизацию в каждой точке, где производится доступ к глобальной переменной. В реальной программе при- шлось бы найти все такие места и добавить в них синхронизацию. Наличие структуры положительно сказывается и на повторном использовании. Выделив повторно используемые компоненты, разработчик получает возможность применять их многократно как в данном, так и в других приложениях. В рассмот- ренных выше примерах нет ничего, что можно было бы использовать повторно. Чтобы проект приложения обладал свойствами продуктивности, расширяе- мости, производительности и повторной используемости, он должен удовлетво- рять следующим требованиям: □ простота перехода от ПК к КПК с минимальным числом изменений; □ редактор с поддержкой перетаскивания элементов управления в графиче- ский интерфейс;
Резюме !!!! 61 □ оформление обработчиков сообщений в виде отдельных функций, вызы- ваемых из предложения switch; □ автоматизация и сокрытие деталей анализа сообщения; □ наличие менеджера глобальных данных с методами доступа; □ выделение повторно используемых функций с целью сокращения исход- ного текста программы. Приведенные выше примеры не удовлетворяют ни одному из этих требова- ний. И тем не менее подобные программы берутся за основу во многих книгах, посвященных программированию для Windows СЕ. ПРИМЕЧАНИЕ В следующей главе мы займемся реализацией каркаса, в котором будут учтены все сформулированные требования. Резюме В этой главе показан способ реализации простой Windows-программы для настольного ПК и последующего преобразования ее для работы на платформе Windows СЕ. Также были вскрыты недостатки подхода к проектированию, встре- чающегося во многих книгах по программированию для Windows СЕ. Вот что сле- дует запомнить: □ В программах для Windows СЕ используются типы WCHAR или TCHAR, и это лишний раз доказывает, что Windows СЕ - сокращенная версия Windows 2000. □ Функция WinMain регистрирует оконный класс приложения, а затем со- здает окно этого класса. □ Функция WinMain в цикле выбирает сообщения из очереди основного по- тока и обрабатывает их в порядке поступления. □ Функция WinProc содержит предложение switch, в котором сообщения передаются подходящим обработчикам в зависимости от типа. □ В большинстве книг, посвященных Windows СЕ, применяется неудачный подход к проектированию, для которого характерны низкая продуктив- ность работы программистов, нерасширяемость, невысокая производи- тельность и отсутствие возможностей для повторного использования. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание_____________________________________Папка Простая Windows-программа для настольного ПК HelloWorld Простая Windows-программа для Pocket PC HelloWorld
62 «Illi Типичные программы для Pocket PC Инструкции по сборке и запуску Программа для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект Hello World.dsw в папке Hello World. 3. Соберите программу. 4. Запустите программу. 5. Попробуйте переместить окно, потянув за полосу заголовка. Хотя выгля- дит это и «коряво», но тем не менее окно приложения останется в левом верхнем углу экрана. 6. Нажмите кнопку РК. 7. Окно закроется, так как приложение завершило работу. Программа для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект HelloWbrldPPC.vcw в папке HelloWorldPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. И. Запустите программу HelloWorld. 12. Попробуйте переместить окно, потянув за полосу заголовка. Ничего не произойдет, и окно останется в левом верхнем углу экрана. 13. Коснитесь кнопки ОК стилосом. 14. Окно закроется, так как приложение завершило работу.
1 и и нн ннн HHHi НИНИ1 н^нн http: //all - ebooks. com Глава 3. Минимальная легко тестируемая программа для Pocket PC В предыдущей главе мы раскритиковали типичный подход к разработке про- грамм для Windows СЕ и Pocket PC, а в этой займемся разработкой альтернатив- ного решения. Графический интерфейс минимальной, легко тестируемой про- граммы для Windows СЕ - это пример стандартного интерфейса, учитывающего ограниченный размер экрана КПК. Структура программы отвечает всем требо- ваниям, сформулированным в предыдущей главе. Инструмент - Message Cracker Wizard (мастер анализаторов сообщений) - позволяет без труда включать новые обработчики сообщений. Мы опишем пошаговую процедуру, следуя которой раз- работчик сможет относительно быстро создать заготовку приложения со слож- ным пользовательским интерфейсом. В конце главы мы детально проанализиру- ем, как с помощью описанного подхода удалось достичь заявленных целей. Пользовательский интерфейс минимальной программы для Pocket PC В этом разделе мы опишем графический интерфейс минимальной программы для Pocket PC. Основная наша задача - продемонстрировать стандартный подход к реализации интерфейсов, рассчитанных на маленький экран. Интерфейс мини- мальной программы изображен на рис. 3.1. На первый взгляд, интерфейс мало чем отличается от описанного в предыду- щей главе. Но посмотрите внимательнее. Здесь имеется полоса меню, в которой есть пункт Quit, позволяющий пользователю завершить программу. Применение меню - осознанный выбор. Меню занимает мало места на экране. Под кнопку же отводится недопустимо много драгоценной экранной площади. Сравните размеры кнопки (рис. 2.1 из предыдущей главы) с размером полосы меню. Еще надо принять во внимание, что операция завершения выполняется все- го один раз за время работы с программой. Поместив пункт Quit в полосу меню, мы освобождаем место для содержательной информации. Мы будем всегда располагать пункт Quit первым в полосе меню. Поэтому пользователь будет знать, где искать элемент, позволяющий выйти из программы (или вернуться на предыдущий уровень интерфейса, для чего мы будем использо- вать пункт меню Return). Многие программы располагают этот пункт справа. Но поскольку число пунктов меню в разных программах разное, то при таком подхо- де пункт Quit или Return окажется плавающим. Программой удобнее пользовать- ся, если он всегда находится в фиксированной позиции слева.
64 III Легко тестируемая программа для Pocket PC Minimal Dig Program ♦L Quit I . Йх*? Стандартная полоса меню Рис. 3.1. Графический интерфейс минимальной программы для Pocket PC ПРИМЕЧАНИЕ Отметим, что программа для Pocket PC дает ровно один способ выполнить ту или иную операцию. Мы будем возвращаться к этому тезису на протяжении всей кни- ги. Откровенно говоря, предоставление единственного способа решения задачи идет вразрез с принятой идеологией проектирования интерфейсов пользова- теля. В современных книгах на эту тему недвусмысленно утверждается, что у пользователя должно быть несколько вариантов для достижения желаемого ре- зультата. Для программ, работающих на компьютере с большим экраном, мышью и полноценной клавиатурой, это действительно так. Но у КПК крохотный экран, стилос вместо мыши и почти бесполезная клавиатура. Поскольку условия работы столь сильно различаются, то и принципы проектирования программ для на- стольных ПК не подходят. Проектирование пользовательского интерфейса для программы, работающей на КПК, - непростая задача. Необходимо приложить все усилия для эффективно- го использования экрана. Проектирование минимальной программы для Pocket PC В основе представленной в этой главе минимальной программы лежит диалог. С точки зрения пользователя, между окном и диалогом нет нш 1 kiп'‘ ’гиды. На
Проектирование минимальной программы самом деле диалог - просто частный случай окна. Но программист, разрабатыва- ющий диалоги, вправе принять определенные допущения и воспользоваться ин- струментами, облегчающими работу. На рис. 3.2 представлен проект программы, реализующей минимальный диалог. Каждый прямоугольник на диаграмме соответствует одной функции или ком- поненту программы. В верхней части прямоугольника написано имя функции. С некоторыми прямоугольниками связаны прямоугольники поменьше, выступа- ющие за границу основного. Они представляют конкретные функции, предостав- ляемые данным элементом. На диаграмме показаны также взаимодействия между различными элементами. В описании взаимодействия есть два важных момента: инициатор и поток данных. Длинная стрелка описывает одно взаимодействие между двумя элемен- тами. Тот элемент, из которого она исходит, является инициатором. Выше стрел- ки взаимодействия расположена короткая стрелка, описывающая поток данных. Она направлена в ту сторону, куда передаются данные. Рис. 3.2. Проект минимальной программы для Pocket PC Следует также отметить индикаторы связи. Иногда диаграмма становится слишком громоздкой. Индикатор связи позволяет разорвать взаимодействие и продолжить его описание в другом месте. Такие индикаторы представляются кру- жочком с буквой. Одинаковые буквы обозначают одно разорванное взаимодейст- вие. Понятно, что этот прием служит лишь для облегчения восприятия диаграммы. Ниже описаны все элементы, представленные на рис. 3.2. □ DlgForm - сценарий, содержащий шаблон диалога; является ресурсом.
66 Illi Легко тестируемая программа для Pocket PC □ DlgMain - главная программа, запускающая диалог, описание которого хранится в ресурсе. □ OnlnitDlg (и прочие On-функции) - функции, реализующие обработчики конкретных сообщений Windows. □ DataMgr - репозитарий разделяемых данных, предоставляющий методы доступа к ним. □ PortabilityUtils - вспомогательные функции, отображающие операции программы на функции API для конкретной платформы. □ IFiles.h - файл, содержащий флаг, управляющий выбором целевой плат- формы. На первый взгляд, для простой программы здесь слишком много всего. Дейст- вительно, в предыдущей главе нам хватило лишь функций WinMain и WinProc. Но ниже мы убедимся, что такой подход обеспечивает продуктивность, расширя- емость, производительность и возможность повторного использования. В начале работы эта программа вызывает функцию WinMain, код которой на- ходится в файле DlgProc. Она передает описатель экземпляра Instance менеджеру DataMgr с помощью метода доступа PutProgramlnstance. Затем WinMain получа- ет идентификатор диалога DialogID от DlgForm и запускает диалог. Функция DlgProc выбирает сообщения из очереди основного потока и передает их подходя- щим обработчикам, например DlgOnCommand. Обработчики выполняют опера- ции, специфичные для конкретного приложения. Иногда для этого приходится обращаться к вспомогательным функциям из компонента Portability Utils, напри- мер DisplayAMenu. В зависимости от флага, установленного в файле IFiles.h, вы- бирается та реализация функции, которая будет правильно работать на целевой платформе: Windows 2000 или Windows СЕ. На диаграмме ясно видна необходимость репозитария DataMgr. Он предо- ставляет всей остальной программе доступ к описателю экземпляра. При этом гарантируется сохранение значения в промежутке между моментами выполне- ния разных обработчиков, а он может достигать нескольких минут. Функция WinMain в программе DlgMain заносит описатель экземпляра в DataMgr, обраща- ясь к функции PutProgramlnstance. Позже, когда из очереди будет выбрано сооб- щение WM_CREATE, его обработчик OnlnitDlg запросит описатель у DataMgr. Стрелка взаимодействия (снабженная индикатором связи) показывает, что для этой цели OnlnitDlg обращается к функции GetProgramlnstance и в результате получает описатель Instance. Следовательно, направление потока данных проти- воположно направлению стрелки взаимодействия. ПРИМЕЧАНИЕ , Менеджер данных играет роль класса C++ в программе, написанной на С. Сохра- няемые данные аналогичны закрытым данным-членам в классе C++, хотя реали- зовано это несколько иначе. Ясно, что функции доступа - это аналоги открытых функций-членов в C++. Подобное использование абстракций будет встречаться в настоящей книге часто.
Анализаторы сообщений IIIHIHMIHH Некоторые второстепенные детали на диаграмме опущены, но основные ком- поненты присутствуют. При составлении таких диаграмм надо стараться отде- лять важное от второстепенного, не жертвуя ясностью представления. Если бы на диаграмме были показаны мелкие детали, то она стала бы слишком громоздкой и не смогла бы передать структуру программы. Проектная диаграмма на рис. 3.2 обладает еще одной важной характеристи- кой. Компоненты образуют связанные друг с другом уровни, а именно: □ уровень управления и задания последовательности. За прохождение со- общений по приложению отвечают функции DlgMain и DlgProc; □ уровень функциональной обработки. Реакцию программы на сообщения обеспечивают обработчики сообщений, например, OnlnitDlg; □ уровень управления данными и интерфейсами. Этот уровень отвечает за доступ к разделяемым данным и специфическим интерфейсам, например, к аппаратуре и особенностям целевой платформы. Выделение уровней в проекте - ключевая тема настоящей книги. Мы еще нео- днократно будем возвращаться к ней ради выполнения сформулированных в пре- дыдущей главе требований. ПРИМЕЧАНИЕ Структурирование - это необходимое условие для достижения продуктивности, расширяемости, производительности и повторной используемости. Достаточ- ным условием служит отнесение каждого компонента к одному из четко опреде- ленных уровней. Определение уровней, взаимодействие между которыми огра- ничено, уменьшает сложность программы. А получение богатой функциональности при минимальной сложности и ограниченном взаимодействии способствует до- стижению заявленных целей. Анализаторы сообщений Выше мы не обсудили одну важную деталь, относящуюся к механизму пере- дачи входных данных от процедуры DlgProc обработчикам сообщений. Здесь есть два аспекта. Процедура DlgProc должна выделить параметры сообщения из эле- ментов wParam и IParam, а затем направить их подходящему обработчику. То и другое обеспечивают анализаторы сообщений. Прежде чем перейти к деталям, рассмотрим общую архитектуру процесса ана- лиза сообщений, представленную на рис. 3.3. На этой диаграмме применены те же обозначения, что и на предыдущей. В верхней части изображены различные компоненты анализатора сообщений, а в нижней - сами обработчики и их взаимодействия с процедурой DlgProc. В распоряжении разработчика приложений для Pocket PC имеется специаль- ный инструмент - Message Cracker Wizard, который генерирует код, необходи- мый для подготовки сообщения для конкретного обработчика.
68 .III! Легко тестируемая программа для Pocket PC ПРИМЕЧАНИЕ Этот инструмент не является продуктом компании Microsoft. Он появился, потому что мне был нужен автоматизированный способ анализа сообщений для различ- ных программ для Pocket PC. Инструмент генерирует объявление обработчика сообщений, ветвь переклю- чателя switch на основе макроса HANDLE_DLG_MSG и заготовку тела обработ- чика. Как видно из рис. 3.3, макрос HANDLE_DLG_MSG находится в заголовоч- ном файле windowsy.h. InputMsg Рис. 3.3. Процесс анализа сообщений для передачи обработчикам ПРИМЕЧАНИЕ Этот заголовочный файл не поставляется Microsoft, но основан на файле windowsx.h. В файле windowsx.h содержится набор анализаторов сообщений для оконных процедур. Но аргументы и возвращаемые значения диалоговой и оконной процедур существенно отличаются. Как и сам мастер Message Cracker Wizard, этот файл - плод моих усилий по реализации системы анализа сообщений. Параметры сообщения, поступающего диалоговой процедуре, представлены в виде wParam и iParam, как и в любой оконной процедуре, удовлетворяющей требованиям Взаимодействия с Windows. Внутри предложения switch макрос HANDLE_DLG_MSG разбирает сообщение следующим образом. 1. Генерирует ветвь case, соответствующую коду сообщег
Анализаторы сообщений 2. Выделяет из wParam и IParam параметры для обработчика конкретного со- общения. 3. Генерирует предложение, которое вызывает обработчик сообщения, пере- давая ему подготовленные параметры. 4. Генерирует предложение return, которое возвращает нужное значение. Каждый макрос HANDLE_DLG_MSG выполняет одни и те же действия, но результат оказывается зависящим от типа сообщения. СОВЕТ Внутренний механизм работы анализатора сообщений описан в главе 5 в разделе «Ввод и отображение символов», где описывается, как реализовать анализатор пользовательского сообщения. Ниже показаны необходимые элементы анализатора сообщения: прототип функции-обработчика, сам анализатор и функция, обрабатывающая сообщение WM_COMMAND. // Для доступа к макросу анализа сообщений ♦include "windowsy.h" // Объявление обработчика void DlgOnCommand(HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify) ; // Анализатор сообщений в предложении switch внутри диалоговой процедуры HANDLE_DLG_MSG( hDlg, WM_COMMAND, DlgOnCommand ); // Функция-обработчик DlgOnCommand(HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify) { ) Включать файл windowsy.h необходимо для доступа к макросу HANDLE_DLG_MSG. В объявлении функции DlgOnCommand видно, какие ар- гументы передаются обработчику сообщения. Когда в предложение switch внутри диалоговой процедуры вставляется макрос HANDLE_DLG_MSG, программа подставляет в качестве hDlg описатель диалогового окна, в качестве кода сообще- ния - WM_COMMAND, а в качестве имени обработчика - DlgOnCommand. СОВЕТ В заголовочном файле windowsy.h для каждого обработчика имеется коммента- рий, описывающий его сигнатуру. Руководствуясь этими комментариями, про- граммист может вручную добавлять код в функцию DlgProc. Как ни странно, макросу не передаются параметры wParam и IParam. На са- мом деле при его расширении генерируется код, который разбирает эти парамет- ры и конструирует вызовы необходимых функций. Поскольку компилятор рас- ширяет макрос до начала исполнения программы, то в результирующем коде имена wParam и IParam будут присутствовать. Программа откомпилируется кор- ректно, потому что расширенный код находится в области видимости диалоговой процедуры, получаюг и IParam в качестве аргументов.
70 Illi Легко тестируемая программа для Pocket PC Работа с мастером Message Cracker Wizard Работать с мастером Message Cracker Wizard нетрудно. Его интерфейс прост, для получения результата достаточно нескольких щелчков мышью. В этом разде- ле мы по шагам разберем, что необходимо сделать для генерирования кода анали- за одного конкретного сообщения. Для некоторых сообщений мастер генерирует более детальную заготовку тела функции-обработчика в соответствии с предъяв- ляемыми Windows требованиями. На рис. 3.4 показан основной интерфейс мастера Message Cracker Wizard. Цифры обозначают последовательность шагов, которые пользователь должен вы- полнить для генерирования кода. Hi Message Cracker Wizard HSE3i Message Selection | Code Review) Data Review] Instructions) 3- 6 | Message List WM.ACTIVATE WM_ACTIVATEAPP WM.CHAR WM.CLEAR WM CLOSE Window Procedure <• Dialo^Procedure Save Selections 1 ,i и Generate Code Select Minimal Msgs || Select Dialog Msgs || ====&====!-. = Select All Messages | OK http: //all - ebooks. com Рис. 3 4 Основной интерфейс мастера Message Cracker Wizard Пользовательский интерфейс состоит из нескольких вкладок, соответствую- щих следующим операциям. □ Выбор сообщения (Message Selection) - позволяет выбрать одно или не- сколько сообщений. □ Просмотр кода (Code Review) - выполняет генерирование кода для ана- лиза выбранных сообщений. □ Просмотр данных (Data Review) - позволяет просмотреть информацию о сигнатуре обработчиков выбранных сообщений. □ Справка (Instructions) — онлайновая справочная информация о работе : мастером.
Работа с мастером Message Cracker Wizard Н1ННИИИ 71 Для большей части пользователей интерес представляют в основном первые две вкладки, поскольку именно с их помощью выполняется генерация кода. Чтобы выбрать одно или несколько сообщений, для которых впоследствии будет сгенерирован код, выполните следующие действия (см. номера шагов на рис. 3.4). 1. Щелкните по переключателю Dialog Procedure, чтобы мастер работал в ре- жиме генерирования кода для диалоговой процедуры. 2. Нажмите кнопку Clear Selections, чтобы очистить внутренний список вы- бранных сообщений. 3. В списке отметьте одно или несколько сообщений. 4. Нажмите кнопку Save Selections, чтобы поместить выбранные сообщения во внутренний список. 5. Нажмите кнопку Generate Code, чтобы начать обработку внутреннего списка. 6. Перейдите на вкладку Code Review, чтобы просмотреть и скопировать сге- нерированный код. Остальные кнопки, расположенные вдоль нижнего края окна, соответствуют различным специальным наборам сообщений. Так, при нажатии кнопки Select Dialog Msgs в списке Message List будут выделены сообщения WM_INITDIALOG и WM_COMM AND. При нажатии кнопки Select Useful Msgs выбираются и копи- руются во внутренний список сообщения WM_CREATE, WM_COMMAND, WM_PAINT и WM_DESTROY. После выполнения всех описанных шагов пользователь увидит окно, изобра- женное на рис. 3.5. Для просмотра и копирования сгенерированного кода выполните следующие действия (см. номера шагов на рис. 3.5). 1. Прокрутите верхнее окно, чтобы просмотреть список сгенерированных объявлений функций-обработчиков. 2. Прокрутите среднее окно, чтобы просмотреть код диалоговой процедуры со вставленными в нее макросами. 3. Прокрутите среднее окно, чтобы просмотреть тела функций-обработчиков. 4. Нажмите кнопку Copy All text, чтобы скопировать содержимое всех трех окон в буфер обмена. После этого можно скопировать содержимое буфера обмена в окно редактора в Visual C++. Правда, придется еще поместить фрагменты сгенерированного кода в нужные места внутри функции DlgProc. Этот инструмент предоставляет несколько способов копирования. Так, кноп- ки справа позволяют скопировать содержимое одного какого-либо окна. А с помо- щью клавиатуры и мыши можно поместить содержимое любого из трех окон со сгенерированным кодом в буфер обмена. Иногда проще скопировать в файл с про- цедурой DlgProc не сразу весь код, а по частям. В алгоритм генерирования кода встроены некоторые дополнительные удоб- ства. В верхнем окне присутствует предложение #include «windowsw.h» для вклю- чения заголовочного файла с прототипами обработчиков сообщений. В среднем окне вы найдете полный код диалоговой процедуры, включающий предложение switch и анализаторы i 'ранных сообщений. И наконец, тела многих функ-
72 Illi Легко тестируемая программа для Pocket PC И Message Cracker Wizard Message Selection Code Review | Data Review | Instructions) void DlgOnCommand (HWND hDlg. int ilC *. [BOOL CnlnttDialog (HWND hDlg HWND ~ HANDLE DLG_MSG(hOlg WM_COMMAf_^_ HANDLE DLG MSGfhDIc WMJNITDIAL » /oid DlgOnCommand (HWND hDlg mt ilD | j Headier Prototypes | WndProc Body Copy Selected Text I j Handler Bodies Copy Selected Text | Рис. 3.5. Вкладка Code Review в окне мастера Message CrackerWizard ций-обработчиков уже содержат код, который, вероятнее всего, понадобится пользователю. К числу таких функций относятся обработчики сообщений WMJNITDIALOG, WM_COMMAND, всех сообщений типа WM_CTLCOLOR, например WM_CTLCOLORSTATIC и WM CTLCOLORBUTTON, а также сооб- щения WM_PAINT. Конечно, сгенерированный по умолчанию код еще придется модифицировать под нужды конкретного приложения, но само его наличие повы- шает продуктивность разработчика. Вот, например, какой код генерируется для обработчика сообщения WM_PAINT: void OnPaint ( HWND hDlg ) { HDC hdc ; PAINTSTRUCT ps ; hdc = BeginPaint(hDlg, &ps) ; EndPaint(hDlg, Sps) ; } При обсуждении структуры обработчика этого сообщения в предыдущей гла- ве мы видели, почему этот минимальный код необходим. После того как мастер сгенерировал его автоматически, программисту остается включить зависящие от приложения детали между вызовами BeginPaint и EndPaint. На вкладке Data Review (рис. 3.6) программист найдет краткую информацию об обработчике каждого сообщения.
Реализация минимального диалога 73 Чтобы на этой странице что-то появилось, пользователь должен хотя бы со- хранить во внутреннем списке выбранные сообщения. И Message Cracker Wizard ВВЕЗ Message Selection | Code Review Data Review | Instructions | MessageName >M_ACTIVA’ - /M_ACTIVATF- M_CHAR Al CLEAR < -M_CLOSE • ALCOMMAND >M„COF¥ .vMCREATE vM_CTLCOLOF© ' A1_CTLCOLCRD ,M_CTLCOLORE M CTLCOLORU | Return Type | Default Ratum Value | Func wt VOfd void void void void BOOL DWORD DWORD DWORD DWORD UUI L nuu NULL NULl NUU. NULL TRUE NULL NULL NULL NULL C- Рис. 3.6. Вкладка Data Review в окне мастера Message Cracker Wizard Для просмотра информации нужно выполнить два простых шага (обозначен- ные на рис. 3.6 цифрами): □ прокрутить список по вертикали до строки, соответствующей нужному со- общению; □ с помощью горизонтальной полосы прокрутки вывести для обозрения ко- лонку, содержащую сигнатуру обработчика. В каждой строке приводится исчерпывающая информация о соответствую- щем обработчике. В первой колонке вы видите тип сообщения, например WM_COMMAND; в следующих двух колонках - тип возвращаемого значения и значение по умолча- нию. Затем идут имя функции-обработчика и число ее аргументов. И наконец, список аргументов обработчика - по две колонки на каждый аргумент. В одной указан его тип, а в другой - имя формального параметра. Реализация минимального диалога В этом разделе мы подробно рассмотрим каждый элемент минимальной диа- логовой программы Как и выше, сначала приводится полный исходный текст,
74 III! -Легко тестируемая программа для Pocket PC а затем следует его построчный анализ. Мы обсудим шаблон диалога, функцию WinMain, диалоговую процедуру, обработчики сообщений и вспомогательные функции, относящиеся к компоненту PortabilityUtils. Шаблоны диалогов и меню Одна из причин, по которым мы используем приложения на основе диалого- вых окон, заключается в том, что для конструирования пользовательского интер- фейса и меню имеется редактор ресурсов. Он поддерживает технологию перетаски- вания мышью. Программисту нужно всего лишь перетащить элемент управления из панели ресурсов на рабочую поверхность диалога. Во время сохранения редак- тор создает шаблон диалога, который затем компилируется и компонуется с ис- полняемой программой. Приведенный ниже фрагмент содержит полный шаблон диалога, сгенериро- ванный редактором ресурсов, входящим в Visual C++: // Из файла resource.h #define IDD_DIALOG1 101 // Из файла DlgForm.rc IDD_DIALOG1 DIALOG DISCARDABLE 0, 0, 155, 157 STYLE DS_MODALFRAME I WS_POPUP I WS_CAPTION CAPTION "Minimal Dig Program" FONT 8, "MS Sans Serif” BEGIN END Описание ресурса состоит из двух файлов. В файле resource.h находится сим- волическая константа, представляющая уникальный идентификатор ресурса, в данном случае IDD_DIALOG1. Когда программист создает новый диалог, ре- дактор ресурсов автоматически присваивает ему идентификатор. Шаблон диало- га хранится в файле сценария, в данном случае он называется DlgForm.rc и содер- жит описание диалога DlgForm, показанного на рис. 3.2. Как видно, в rc-файле вслед за идентификатором диалога идут ключевые сло- ва DIALOG DISCARDABLE, а затем координаты левого верхнего угла диалогового окна и его размеры. Той другое измерено в условных единицах, которые Windows преобразует в физические пиксели на этапе отображения окна. И СОВЕТ Окно размером 155х 157 в условных единицах заполняет почти весь физический экран КПК, что согласуется с нашей целью - вынудить пользователя в каждый момент времени работать только над одной задачей. При разработке приложе- ния программисту, возможно, придется изменить эти значения, хотя для боль- шинства КПК они годятся. Редактор ресурсов позволяет задать для диалога стилевые параметры. В дан- ном случае мы указали, что это модальный диалог с рамкой (DS_MODALFRAME), отображаемый во всплывающем окне (WS_POPUP), имеющем полосу заголовка
Реализация минимального диалога 1ИИП 75 (WS_CAPTION). В полосе заголовка будет отображаться строка «Minimal Dig Program». Все элементы управления, рисуемые в окне диалога, будут размещены между операторными скобками BEGIN и END. В результате компиляции шаблон диалога преобразуется в структуру данных, которая включается в исполняемую программу. Для доступа к ней необходимы опи- сатель экземпляра приложения и идентификатор диалога, то есть IDD_DIALOG1. При создании меню также генерируется шаблон ресурса, на этот раз меню. Ниже показан шаблон главного меню нашего приложения: // Из файла resource.h ♦define IDD_MENU1 102 // Из файла DlgForm.rc IDR_MENU1 MENU DISCARDABLE BEGIN MENUITEM «Quit», IDOK END Константа IDR_MENU1 в файле resource.h однозначно идентифицирует ре- сурс. Идентификатор автоматически создается редактором. Само описание меню находится в файле DlgForm.rc. Вначале идет идентификатор меню и ключевые слова MENU DISCARDABLE, затем в операторных скобках BEGIN ... END - описания пунктов меню в виде. Описание каждого пункта начинается со слова MENUITEM, за которым следуют текст пункта и его идентификатор. ПРИМЕЧАНИЕ Уникальный идентификатор пункта меню включается в состав сообщения WM_COMMAND, отправляемого, когда пользователь щелкает по этому пункту мышью. Символ IDOK встречается почти в любом описании пользовательского ин- терфейса. Поэтому он уже определен в файле windows.h и, следовательно, отсутст- вует в файле resource.h Функция WinMain Функция WinMain является точкой входа в программу, поэтому с нее мы и начнем обсуждение. Ниже приведен полный исходный текст этой функции. /*********************************************** * File: DlgMain.с * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ♦include <windows.h>
1111 Легко тестируемая программа для Pocket PC ♦include <windowsx.h> ♦include "resource.h” ♦include "DataMgr.h" BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam) ; // Определение WinMain int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR IpCmdLine, int nCmdShow) PutProgramlnstance(hlnstance) ; DialogBox( hlnstance, MAKEINTRESOURCE(IDD_DIALOG1) HWND_DESKTOP, (DLGPROC) DlgProc ) ; return 0 ; Сравните этот текст с приведенным в предыдущей главе. Нам понадобилось всего три строки в теле функции. В первой из них мы с помощью функции PutProgramlnstance помещаем описатель экземпляра hlnstance в хранилище разде- ляемых данных, управляемое компонентом DataMgr. Вызов функции DialogBox во второй строке приводит к появлению диалогового окна на экране. На первый взгляд, отсутствуют важнейшие части: регистрация диалоговой процедуры в оконном классе приложения, создание главного окна и цикл выбор- ки и обработки сообщений. Но они никуда не делись, просто Win32 API реализует их внутри функции DialogBox. Программисту вообще не нужно их кодировать! Q J ПРИМЕЧАНИЕ Не имея доступа к циклу выборки сообщений, программа кое-что теряет, а имен- но возможность воспользоваться клавишами-акселераторами. Акселератор - это комбинация клавиш, позволяющая выбрать некоторый пункт меню. Но веро- ятность того, что акселераторы потребуются в программе для Pocket PC, мала. Ведь обычно акселератор включает несколько клавиш, а в случае Pocket PC «клави- атура» рисуется на экране и пользователь «нажимает» клавиши с помощью стило- са. Для нажатия одновременно нескольких клавиш существует очень ограниченная поддержка. Поэтому утрата доступа к акселераторам - небольшая потеря. Самая важная строка в функции WinMain - та, что открывает диалоговое окно: DialogBox( hlnstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, (DLGPROC) DlgProc ) ;
Реализация минимального диалога 77 Аргументы несут важную информацию. Первым передается описатель экземп- ляра приложения hlnstance. Без него нельзя было бы добраться до ресурса, содер- жащего описание диалога. Следующий аргумент - идентификатор ресурса. Как ви- дим, идентификатор IDD_DIALOG1 погружен в макрос MAKEINTRESOURCE, определенный в файле windows.h. Этот макрос преобразует целочисленный иден- тификатор в строку, как того требует функция DialogBox. Третий аргумент - опи- сатель родительского окна. В данном случае диалоговое окно является главным окном приложения, поэтому родительским для него будет рабочий стол, на что указывает предопределенный символ HWND_DESKTOP. И последний, четвер- тый, аргумент - это указатель на диалоговую процедуру DlgProc, которая обраба- тывает все поступающие окну сообщения. После того как эти строки написаны, программист может забыть о функции WinMain. Все остальное происходит внутри DlgProc. Функция DlgProc В любой Windows-программе рабочей лошадкой является функция обработки сообщений. Функция DlgProc в данном приложении сильно отличается от функ- ции WinProc, которую мы рассматривали в предыдущей главе. И причина не в том, что это диалоговое, а не какое-то другое окно, а в применении анализаторов для диспетчеризации сообщений. Вот полный исходный текст диалоговой проце- дуры DlgProc. /*********************************************** * File: DlgProc.с * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ ♦include <windows.h> ♦include <windowsx.h> ♦include "resource.h" ♦include "windowsy.h" BOOL void OnlnitDialog ( HWND hDlg, HWND hDlgFocus, long UnitParam ) ; DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) ; void void DWORD DlgOnPaint(HWND hDlg) ; DlgOnMove ( HWND hDlg, int x, int у ) ; DlgOnCtlColorStatic ( HWND hDlg, HDC hDC, HWND hDlgChild, UINT msgCode ) ; ♦include <tchar.h> ♦include "IFiles.h" ♦include "DataMgr.h" ♦include "PortabilityUtils.h"
78 1111 Легко тестируемая программа для Pocket PC BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) { switch (message) { HANDLE_DLG_MSG( hDlg , WM_INITDIALOG , OnlnitDialog ) ; HANDLE_DLG_MSG( hDlg , WM_COMMAND , DlgOnCommand ) ; HANDLE_DLG_MSG( hDlg , WM_PAINT , DlgOnPaint ) ; HANDLE_DLG_MSG( hDlg , WM_MOVE , DlgOnMove ) ; HANDLE_DLG_MSG( hDlg , WM_CTLCOLORSTATIC , DlgOnCtlColorStatic ) ; ) return FALSE ; } Опять-таки сравните с текстом функции WinProc из предыдущей главы. Этот вариант яснее, проще и лучше структурирован. Анализ параметров сообщений происходит внутри макроса HANDLE_DLG_MSG. Дополнительное преимуще- ство заключается в том, что все детали обработки конкретных сообщений вынесе- ны в отдельные функции и не загромождают текст DlgProc. При такой структуре удается изолировать обработчики друг от друга и, как следствие, сократить время отладки. Единственный элемент диалоговой процедуры, который мы еще не обсужда- ли, - это предложение BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) В объявлениях диалоговой и оконной процедур есть сходства и различия. Сходство очевидно - аргументы практически одинаковы. Тип первого аргумента тот же, что и раньше, но он переименован и напоминает, что речь идет о диалого- вом окне. Основные различия - тип возвращаемого значения и уточнение. Диалоговая процедура возвращает значение TRUE или FALSE типа BOOL. Но программисту об этом думать не надо, так как макрос HANDLE DLG MSG сам генерирует под- ходящее значение, возвращаемое обработчиком сообщения. СОВЕТ Макросы, определенные в файле windowsx.h, возвращают значения типа long. Поэтому они не подходят для использования в диалоговых программах. Анализа- торы же сообщений, находящиеся в файле windowsy.h (написанном автором), возвращают значение типа BOOL, что согласуется с требованиями диалоговой процедуры. В прототипе диалоговой процедуры присутствует уточнение CALLBACK. На самом деле в файле windows.h этот символ определен как WINAPI, то есть не от- личается от уточнения оконной процедуры WinProc. Напомним, что символ WINAPI требует, чтобы компилятор генерировал код очистки стека внутри вызы- ваемой функции (а не возлагал эту обязанность на вызывающую функцию, как обычно принято в С). Символы CALLBACK и WINAPI взаимозаменяемы.
79 Реализация минимального диалога 1И11ПН Тела обработчиков сообщений Обработчики сообщений реализуют функционал приложения. Каждый из них занимается обработкой сообщений одного типа. Обработка заключается в выполнении логической последовательности вызовов различных компонентов из слоя управления данными и интерфейса. В рассматриваемой минимальной программе эти последовательности, как правило, просты и линейны. Для начала рассмотрим обработчик сообщения WM_INITDIALOG: BOOL OnlnitDialog ( HWND hDlg, HWND hDlgFocus, long UnitParam ) ( HINSTANCE Instance ; Instance = GetProgramlnstance() ; FixWindowPosition( hDlg, 0,0); DisplayAMenu( hDlg, IDR_MENU1, Instance) ; return TRUE ; } В нем нет никаких ветвлений, различные функции вызываются строго одна за другой. Прежде всего интерес представляет первый аргумент hDlg. Это описатель диалогового окна, являющегося главным окном приложения. Диалоговая процедура получает от Windows сообщение WM_CREATE, когда функция DialogBox обращается к CreateWmdows для создания главного окна. Данный обработчик устанавливает начальное положение окна, вызывая функцию FixWindowPosition. Затем он с помощью функции DisplayAMenu выводит меню. Обе эти функции принадлежат низкоуровневому компоненту PortabilityUtils и вызывают те или иные функции API в зависимости от целевой платформы. Для инициализации меню обработчику нужен описатель экземпляра прило- жения. В WinMain мы с помощью функции PutProgramlnstance поместили его в хранилище разделяемых данных, а теперь OnlnitDialog запрашивает описатель у DataMgr, вызывая функцию GetProgramlnstance. Когда пользователь касается стилосом пункта меню Quit, диалоговой про- цедуре посылается сообщение WM_COMMAND. Ниже приведен текст его обра- ботчика. void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { switch( ilD ) { case IDOK: EndDialog(hDlg, 0) ; break ; } } В этом обработчике наиболее интересен второй аргумент ild, в котором переда- ется уникальный идентификатор источника сообщения. Предложение switch пере- дает управление участку кода, который знает, как реагировать на этот источник. Диалог - это и есть приложение, и оно должно как-то завершаться. В функ- ции WinMain из предыдущей главы мы завершали приложение, вызывая
80 Illi Легко тестируемая программа для Pocket PC PostQuitMessage. Для достижения того же результата в Windows служит функция EndDialog. Как и PostQuitMessage, она помещает сообщение WM_QUIT в оче- редь основного потока. В результате цикл выборки сообщений внутри DialogBox завершается, а вместе с ним и все приложение. Как бы ни было создано окно, приложение должно что-то рисовать в его клиент- ской области. Этим занимается обработчик сообщения WM_PAINT: void DlgOnPaint(HWND hDlg) { HDC Devicecontext ; PAINTSTRUCT Paint ; RECT Rectangle ; HBRUSH Brush ; DeviceContext = BeginPaint(hDlg,SPaint) ; GetClientRect(hDlg,&Rectangle) ; Brush = (HBRUSH)GetStockObject(WindowBGColor) ; FillRect(DeviceContext,SRectangle,Brush) ; EndPaint(hDlg,&Paint) ; } Единственный аргумент этого обработчика - описатель окна. Код содержит те же команды рисования, что и функция WinProc из предыдущей главы. Только для улучшения структуры программы мы поместили их в отдельную функцию, а не в само тело диалоговой процедуры. Впрочем, одно отличие все-таки имеется. Идентификатор кисти для закраши- вания клиентской области равен WindowBGColor. Его определение находится в файле IFiles.h: #define WindowBGColor WHITE_BRUSH Поместив этот символ в заголовочный файл, мы предоставляем доступ к нему разным частям программы. Еще одно преимущество заключается в том, что для изменения цвета кисти достаточно переопределить всего один символ. В предыдущей главе обработчик сообщения WM_M0VE фиксировал окно приложения в левом верхнем углу экрана. Теперь его код выглядит следующим образом: void DlgOnMove ( HWND hDlg, int x, int у ) ( MaintainWindowPosition( hDlg, 0,0); } Дополнительные аргументы определяют положение левого верхнего угла окна и извлекаются из параметров сообщения WM_MOVE. Но обработчик не обязан эти аргументы использовать. Тело обработчика состоит всего из одной строки, в которой вызывается функ- ция MaintainWindowPosition, принадлежащая компоненту PortabilityUtils. Ее аргументами служат описатель диалогового окна и экранные координаты левого верхнего угла окна. Поскольку назначение обработчика - зафиксировать окно в левом верхнем углу экрана, то вместо полученных от вызывающей программы параметров х и у мы передаем 0, 0.
Реализация минимального диалога IIIHIBM 81 И наконец, обработчик сообщения WM_CTLCOLORSTATIC. DWORD DlgOnCtlColorStatic ( HWND hDlg, HDC hDC, HWND hDlgChild, UINT msgCode ) { return ((DWORD) GetStockObject(WindowBGColor) ) ; } Ему передается несколько аргументов. hDlgChild - это описатель дочернего окна, в котором отображается статический текст. Зная его, обработчик сообщения может выполнять различные операции над текстом. Еще полезнее аргумент hDC. Это описатель контекста устройства для окна статического текста. Имея его, об- работчик сообщения может вызвать любую функцию GDI (все они требуют кон- текста устройства) для выполнения произвольной операции рисования, напри- мер помещения в окно текста растрового изображения. Как и в случае оконной процедуры, обработчик этого сообщения возвращает описатель кисти для закрашивания окна статического текста. Отметим, что функ- ции GetStockObject в качестве аргумента передается то же значение, которое было использовано для закрашивания клиентской области в обработчике WM_PAINT. Поэтому цвет фона в окне статического текста совпадает с цветом фона всего окна. Чтобы изменить и фоновый цвет статического текста, и фоновый цвет окна, достаточно модифицировать лишь определение константы WindowsBGColor в за- головочном файле IFiles.h. Нет сомнения, что для внесения одного изменения нужно потратить куда меньше усилий, чем на просмотр всего кода в поисках «за- шитых» значений. Компонент PortabilityUtils Назначение этого компонента - обеспечить платформенно-независимый-ин- терфейс с Windows. Необходим он потому, что между некоторыми программными интерфейсами Windows 2000 и Windows СЕ есть существенные отличия. Функ- ции, входящие в состав этого компонента, пользуются флагом WindowsCE, кото- рый находится в заголовочном файле IFiles.h и служит для выбора того или иного варианта компиляции. Вот, например, код функции отображения меню: void DisplayAMenu( HWND Window, int MenuID, HINSTANCE Instance) { #if WindowsCE HWND CBar ; CBar = CommandBar_Create( Instance, Window, IDCB_MAIN) ; ConunandBar_InsertMenubar(CBar,Instance,(WORD)MenuID,(WORD)0) ; #else HMENU MenuHandle ; MenuHandle = LoadMenu( Instance , MAKEINTRESOURCE(MenuID) ) ; SetMenu( Window, MenuHandle ) ; #endif } Важнейшая особенность этой функции - применение директив условной ком- пиляции в зависимости от константы WindowsCE. Если она равна 1, то препроцес-
82 lllll Легко тестируемая программа для Pocket PC сор оставит только код, предназначенный для Windows СЕ. Если же WindowsCE равна 0, то будет откомпилирован код для настольной версии Windows. ПРИМЕЧАНИЕ Путем изменения значения одной лишь константы WindowsCE мы можем сменить платформу, для которой компилируется приложение. Как видно из кода, на платформе Windows СЕ используются функции CommandBar_Create и CommandBar_InsertMenubar, тогда как для Windows 2000 - функции LoadMenu и SetMenu. Различия очевидны и учитываются константой WindowsCE. Функция DisplayAMenu принимает три аргумента. Window - это описатель родительского окна, в которое вставляется полоса меню. У приложения может быть несколько меню с разными идентификаторами. Уникальный идентифика- тор меню передается в аргументе MenuID. Поскольку меню - это ресурс, то для доступа к нему необходим описатель экземпляра приложения. Раньше мы не сталкивались с функциями, использованными в DisplayAMenu, поэтому стоит рассмотреть их подробнее. HWND СВаг ; В Windows СЕ меню располагается в полосе команд, и никакого другого меха- низма не существует. В переменной СВаг хранится описатель полосы команд, ко- торая представляет собой еще одно окно. Помимо меню, в полосе команд могут быть и другие элементы управления, например кнопки и раскрывающиеся списки. СВаг = CommandBar_Create( Instance, Window, IDCB_MAIN) ; Функция CommandBar_Create создает окно, содержащее полосу команд. В качестве аргументов ей передаются описатель экземпляра Instance, описатель родительского окна Window и уникальный идентификатор полосы команд IDCBMAIN. СОВЕТ Если полоса команд располагается в главном окне приложения, как в данном слу- чае, то вы обязаны использовать предопределенный идентификатор IDCB_MAIN, иначе она не появится. CommandBar_InsertMenubar(СВаг, Instance, (WORD)MenuID, (WORD)0) ; После создания полосы команд функция CommandBar_InsertMenubar поме- щает внутрь него меню. Первым аргументом является описатель окна полосы ко- манд СВаг, а поскольку функция должна получить доступ к ресурсу (меню), то необходим также описатель экземпляра Instance и идентификатор этого ресурса MenuID. Последний аргумент - это описатель элемента управления, после кото- рого нужно вставить меню. В этой книге мы всегда будем задавать этот аргумент равным 0
Реализация минимального диалога 1И11Ш 83 Теперь для сравнения рассмотрим, какие операции нужно выполнить для ото- бражения меню в версии Windows для настольного ПК. HMENU MenuHandle ; В этой переменной будет храниться описатель меню. MenuHandle = LoadMenu( Instance , MAKEINTRESOURCE(MenuID) ) ; Эта функция возвращает описатель меню, которое представлено в виде струк- туры данных в памяти. Аргумент Instance - это описатель экземпляра приложе- ния, владеющего меню, a MenuID - его уникальный идентификатор. Макрос MAKEINTRESOURCE преобразует MenuID в строковую форму, необходимую функции. Название LoadMenu выбрано неудачно. Описание меню и так уже находится в памяти загруженной программы. Лучше было бы назвать функцию GetMenu. SetMenu( Window, MenuHandle ) ; Получив описатель, мы с помощью этой функции ассоциируем меню с окном. В качестве аргументов передаются описатель окна-владельца и описатель меню. После возврата из функции меню появится в окне и будет выглядеть так же, как в Windows СЕ. Для фиксации окна в определенном положении тоже применяются разные методы. В одном случае начальное положение окна устанавливается раз и навсег- да, а во втором мы возвращаем его в исходное положение при получении каждого сообщения WM_MOVE. Код, соответствующий первому методу, выглядит так: void FixWindowPosition( HWND Window, int XLocation, int YLocation ) { #if WindowsCE LONG Style ; Style = GetWindowLong(Window, GWL_EXSTYLE) ; Style = Style I WS_EX_NODRAG ; SetWindowLong(Window,GWL_EXSTYLE, Style) ; #else SetWindowPos(Window,NULL,XLocation,YLocation, 0,0,SWP_NOSIZE | SWP_NOZORDER) ; #endif } Напомним, что для возврата окна в исходное положение на настольном ПК используется функция SetWindowPos, тогда как в Windows СЕ для этой цели слу- жит бит расширенного стиля. Механизм работы этого кода разбирался в предыду- щей главе, поэтому больше мы на нем останавливаться не будем. Назначение следующей функции - сохранить положение окна после получе- ния сообщения WM_MOVE: void MaintainWindowPosition( HWND Window , int XLocation, int YLocation ) { #if WindowsCE #else SetWindowPos(Window,NULL,XLocation,YLocation, 0,0,SWP_NOSIZE | SWP_NOZORDER) ; #endif }
Ill | Легко тестируемая программа для Pocket PC В случае Windows СЕ ничего делать не нужно. Коль скоро установлен бит рас- ширенного стиля, запрещающий буксировку окна, Windows СЕ не будет отправ- лять этому окну сообщения WM_MOVE. Ну а раз нет никаких действий, то для платформы Windows СЕ генерируется пустое тело обработчика. Для настольного же ПК вызывается функция SetWindowPos, которая возвращает окно в положе- ние, заданное аргументами XLocation и YLocation. Компонент DataMgr И функции WinMain, и некоторым обработчикам необходим доступ к описате- лю экземпляра приложения. К сожалению, Windows СЕ не позволяет запросить его у операционной системы. Поэтому он заносится в центральное хранилище разде- ляемых данных, управляемое компонентом DataMgr. К нему обращаются и дру- гие разрабатываемые в этой книге приложения для хранения различных данных. Ниже приведен полный текст компонента DataMgr. /★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★it* ★ * File: DataMgr.с * * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ ♦include <windows.h> static HINSTANCE Currentinstance ; void PutProgramlnstance(HINSTANCE Instance ) { Currentinstance = Instance ; } HINSTANCE GetProgramlnstance(void) { return Currentinstance ; } Для создания централизованного менеджера данных нужно решить две зада- чи: предоставить механизм для сохранения данных на протяжении всей жизни программы и реализовать функции доступа. В приведенном выше листинге живучесть данных обеспечивается специфика- тором класса памяти static. У каждого приложения имеется область статических данных, которая существует в течение всего времени работы программы. Кроме того, если в объявлении переменной указан спецификатор static, то она не видна за пределами файла, в котором объявлена. Сокрытие факта существования пере- менной от остальной части программы заставляет обращаться к ней только через функции доступа, что способствует защите внутренних структур данных от мани- пулирования.
Сборка программы для настольного ПК НН» 85 СОВЕТ * Спецификатор класса памяти static - это функциональный аналог закрытого чле- на данных в C++. Можно сказать, что реализованный на языке С компонент DataMgr инкапсулирует закрытые данные. Сложная внутренняя структура данных не видна остальным частям приложения и, следовательно, защищена от несанк- ционированных изменений. Такая форма инкапсуляции для сокрытия внутренней организации применяется в этой книге часто. Для управления инкапсулированным значением описателя экземпляра необ- ходимы только две функции. Одна из них, PutProgramlnstance, сохраняет пере- данное значение описателя в скрытой переменной, а другая, GetProgramlnstance, возвращает его. Сборка программы для настольного ПК Чтобы собрать описанную в этой главе минимальную диалоговую программу для настольного ПК, нужно выполнить следующие действия. 1. Создайте папку для нового проекта, придумав для нее осмысленное назва- ние. 2. Скопируйте в нее все файлы (за исключением имеющих расширения .dsp и .dsw) из папки MinimalDlgProgram. 3. Запустите Visual C++ и выберите проект типа Win32 Application. Введите придуманное вами имя папки в поле Project Name на вкладке New Projects. 4. Выберите пункт меню Project | Add to Project | Files. 5. Включите в проект все файлы с расширениями .с и .h, а также ресурсный сценарий DlgForm.rc. 6. Перейдите на вкладку Resource View в окне Project Explorer и раскройте дерево, так чтобы был виден узел главного диалога IDD_DIALOG1. 7. Дважды щелкните мышью по форме диалога и введите понятное название в поле Caption (Заголовок), находящееся в таблице свойств диалога. 8. Соберите и запустите программу, чтобы убедиться в ее работоспособности. Теперь в программу можно добавить дополнительные возможности. Для рас- ширения функциональности приложения существует целый ряд инструментов. С плавающей панели Tools (Инструменты) можно перетащить в форму другие элементы управления, а мастер Message Cracker Wizard поможет добавить обра- ботчик отправляемых ими сообщений. Перенос программы на КПК Убедившись, что приложение правильно работает на настольном ПК, уже не- трудно перенести его на платформу Windows СЕ для Pocket PC. Для этого необ- ходимо выполнить следующие действия. 1. Создайте папку проекта, назвав ее так же, как для настольной версии про- граммы, но с добавлением в конце строки «РРС».
Illi Легко тестируемая программа для Pocket PC 2. Скопируйте в нее все файлы (за исключением имеющих расширения .dsp и .dsw) из папки, созданной для настольной версии. 3. Запустите Embedded Visual C++ и выберите проект типа WCE Pocket PC Application. Введите имя папки в поле Project Name на вкладке New Projects. 4. Выберите пункт меню Project | Add to Project | Files. 5. Включите в проект все файлы с расширениями .с и .h, а также ресурсный сценарий DlgForm.rc. 6. Выберите пункт меню Project | Settings (Проект | Настройки) и перейдите на вкладку Debug (Отладка). В поле Download Directory (Каталог для заг- рузки) введите \ (корневой каталог). 7. Перейдите на вкладку Link (Компоновщик) в диалоговом окне Project Settings. Удалите суффикс РРС из имени ЕХЕ-файла в поле Output File Name. 8. Закройте окно Project Settings, нажав кнопку ОК. 9. Отредактируйте файл IFiles.h, изменив значение константы WindowsCE на 1. 10. Соберите приложение, чтобы убедиться в том, что все файлы на месте. Если программа компилируется без ошибок, Embedded Visual C++ авто- матически загрузит исполняемый файл на Pocket PC. 11. Запустите программу, чтобы убедиться в ее работоспособности. Если приложение корректно работало на настольном ПК и все вспомогатель- ные функции использованы правильно, то оно должна сразу заработать и на КПК. --- ПРЕДОСТЕРЕЖЕНИЕ__________________________________________ IV ” “ v/Я Л83 из вышеперечисленных шагов особенно важны. Новый проект должен иметь ™п WCE Pocket PC Application. Если выбрать любой другой тип проекта, то компи- дятор выдаст огромное число ошибок. То же произойдет, если вы забудете уста- новить в 1 флаг WindowsCE в заголовочном файле. В этом случае многие функ- ции, входящие в состав компонента PortabilityUtils, не откомпилируются. Если программа исполняется на КПК с ошибкой, придется прибегнуть к помо- щи дистанционного отладчика. Нужда в нем возникает при следующих условиях: □ для исследования Unicode-строк, чтобы выявить различия в обработке по сравнению с ASCII-строками; □ исполнение функций, которые по-разному работают в Windows СЕ и в вер- сиях Windows для настольных ПК; □ исполнение функций, имеющихся в Windows СЕ, но отсутствующих в дру- гих версиях Windows, например для работы с базами данных. Работа с дистанционным отладчиком утомительна и занимает много времени. Обычно программисты подключают КПК по последовательному порту, по кото- рому данные передаются медленно. Можно работать и по сетевому порту при условии, что на платформе разработки есть доступ к DNS-cepsepv
Анализ проекта диалоговой программы 1Н11Ш 87 СОВЕТ При разработке примеров для этой книги нужда в дистанционной отладке воз- никла только однажды. Компонент из главы 10 обращается к «родному» для Windows СЕ менеджеру баз данных. У соответствующих функций нет аналогов в версиях Windows для настольного ПК, так что для исправления ошибок понадо- бился отладчик. Анализ проекта минимальной диалоговой программы В ходе анализа проекта программы в предыдущей главе мы сформулировали критерии, по которым можно судить, обладает ли Windows-программа свойства- ми продуктивности, расширяемости, производительности и возможности повтор- ного использования. Вот как они звучат: □ простота перехода от ПК к КПК с минимальным числом изменений; □ редактор с поддержкой перетаскивания элементов управления в графиче- ский интерфейс; □ оформление обработчиков сообщений в виде отдельных функций, вызыва- емых из предложения switch; □ автоматизация и сокрытие деталей анализа сообщения; □ наличие менеджера глобальных данных с методами доступа; □ выделение повторно используемых функций с целью сокращения исход- ного текста программы. Как легко видеть, программа, разработанная в настоящей главе, отвечает этим требованиям и, следовательно, обладает желаемыми характеристиками. Для переноса программы с ПК на КПК достаточно изменить всего одну кон- станту, это уж точно минимально возможное число изменений. Таким образом, пер- вый критерий удовлетворяется за счет использования компонента Portability Utils и флага WindowsCE в заголовочном файле IFiles.h. Переход к программе, основанной на диалоге, позволил нам воспользоваться операциями перетаскивания для размещения элементов управления на поверхно- сти формы. Программист может точно расположить все элементы без отнимаю- щего много времени повторного компилирования. Применение макроса HANDLE_DLG_MSG позволило выделить обработчи- ки сообщений в отдельные функции. Каждая функция занимается обработкой со- общений одного типа. К тому же мастер Message Cracker Wizard автоматически генерирует необходимый код, повышая как продуктивность, так и степень расши- ряемости приложения. Анализатор сообщений также скрывает детали выделения параметров сооб- щения. Мы уже говорили, что макрос HANDLEDLGMSG получает нужные значения и передает их подходящему обработчику. Для инкапсуляции и контроля доступа к разделяемым данным применяется компонент DataMgr. Данные хранятся в статических переменных и сохраняют значения на ппотяженш то воемени паботы ппиттожения
88 ИН! Легко тестируемая программа для Pocket PC И наконец, в проекте повсеместно встречаются повторно используемые ком- поненты. Доказательством этого утверждения служит последовательность шагов, с помощью которых разработчик быстро создает из этих элементов заготовку но- вого приложения. При первоначальном знакомстве с этим подходом может создаться впечатле- ние, что он излишне сложен, но на самом деле в нем осознанно применяются ком- поненты, позволяющие эффективно решить поставленные задачи. Резюме В настоящей главе разработана минимальная Windows-программа, отвечаю- щая критериям продуктивности, расширяемости, производительности и возмож- ности повторного использования. Перечислим некоторые особенности, которые позволили этого добиться: □ основанная на уровнях структура программы призвана четко определить допустимые взаимодействия между компонентами с целью уменьшения сложности приложения; □ анализаторы сообщений скрывают детали разбора сообщения и передачи параметров обработчику; □ описания диалогов и меню являются ресурсами и хранятся в исполняемом файле программы; □ проект минимальной программы можно положить в основу любого прило- жения для Windows, он позволяет легко выполнять перенос с настольного ПК на КПК; □ использование статических переменных в С дает возможность реализовать тот же уровень инкапсуляции, что закрытые данные-члены в C++. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка Минимальная диалоговая программа для настольного ПК MinimalDIgProgram Минимальная диалоговая программа для Pocket PC MinimalDIgProgramPPC Инструкции по сборке и запуску Минимальная диалоговая программа для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект MinimalDlgProgram.dsw в папке MinimalDIgProgram. 3. Соберите программу. 4. Запустите программу. 5. Попробуйте переместить окно, потянув за полосу заголовка. Хотя выгля- дит это и «коряво», но тем не менее окно приложения останется в левом верхнем углу экрана.
Примеры программ в Web М11ПН 89 6. Выберите пункт меню Quit. 7. Окно закроется, так как приложение завершило работу. Минимальная диалоговая программа для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект MinimalDIgProgramPPC.vcw в папке MinimalDlgProg- ramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу MinimalDlgProgram. 12. Попробуйте переместить окно, потянув за полосу заголовка. Ничего не произойдет, и окно останется в левом верхнем углу экрана. 13. Коснитесь пункта меню Quit стилосом. 14. Окно закроется, так как приложение завершило работу.
MHI Bi I Глава 4. Обзор платформы Pocket PC В примере, разрабатываемом в этой главе, мы покажем, как с помощью графиче- ских примитивов выполнять операции рисования в любом окне. Иными словами, речь пойдет о выводе информации путем обращения к компоненту GDI, являю- щемуся частью Windows. Начать рассмотрение в таком порядке полезно потому, что именно вывод графической информации является основным средством обще- ния программы с пользователем. В следующей главе мы обратимся к средствам ввода данных в программу. Вместо того чтобы просто перечислить графические примитивы и их аргумен- ты, мы опишем их работу в контексте простой анимационной программы. Вы уви- дите, что происходит в клиентской области окна в результате использования той или иной операции рисования. Помимо графических примитивов, программе ани- мации еще понадобятся таймеры и умение перерисовывать клиентскую область окна. Также важно реализовать анимацию так, чтобы избежать мигания на экране. Разработанная в этой главе программа выполняет рисование и анимацию фи- гуры человечка. Простота объекта позволяет не отвлекаться от основной задачи. Наша цель - понять механизм графического рисования, а не заниматься прори- совкой мелких деталей. Впрочем, изображение достаточно реалистично, чтобы результат применения тех или иных программных операций соотносился с визу- альными перемещениями в клиентской области окна. Графический интерфейс пользователя для простой программы анимации В этой главе мы не станем перечислять все графические примитивы, а проде- монстрируем операции рисования на примере очень простой анимационной про- граммы. На рис. 4.1 представлен ее интерфейс. Основная задача приложения - добиться плавного перемещения фигуры по клиентской области окна. Фигурка человечка позволяет увидеть эффект приме- нения графических примитивов, не загромождая программу излишне детальны- ми командами рисования. Мы взяли для примера анимацию еще и для того, чтобы познакомить вас с некоторыми проблемами, возникающими при рисовании в ре- альных программах. На рис. 4.1 описаны существенные особенности программы. С помощью опе- раций рисования фигура составляется из «палочек». Голова изображена в виде круга или эллипса. Чтобы создать впечатление массивности, голова закрашивает- ся каким-нибудь темным цветом, например серым. Туловище, ручки и ножки ри- суются прямыми линиями.
Рисование изображений ШИМ 91 Получена применением нескольких операция рисования Перемещается в ответ на сообщение от таймера Рис. 4.1. Пользовательский интерфейс простой анимационной программы Анимация фигуры управляется таймером. Когда таймер срабатывает, про- грамма перемещает фигуру в другое место. Перемещение состоит из следующих шагов: стереть старую фигуру, вычислить новое место, нарисовать фигуру в но- вом месте. При этом перемещение не должно вызывать мигания. Если программа будет просто стирать содержимое всей клиентской области перед перерисовкой, то мигание неизбежно. Устранить его можно, ограничив пе- рерисовку только изменившейся частью клиентской области. При правильной реализации программа будет стирать лишь прямоугольник, занятый текущим изображением (старую область), и рисовать внутри прямоугольника, содержаще- го новое изображение (новая область). При этом экран обновляется быстрее, и мигание незаметно. Рисование изображений Многие программы выводят информацию в графическом виде. Например, для вывода столбчатой диаграммы применяются операции рисования прямых линий. В этой главе мы рассмотрим работу с графикой в приложении для Pocket PC. Основной упор делается на подробное описание механизма использования имею- щихся примитивов рисования. Все реализации операций рисования обладают некоторыми общими чертами. Поежле всего необходим эр графических инструментов. В него включаются
92 Illi Обзор платформы Pocket PC инструменты, которые будут использоваться в последующих командах рисования. Самыми распространенными инструментами являются перья и кисти. Компонент GDI применяет их, например, для рисования прямых линий и прямоугольников. Кроме того, GDI предоставляет окно для визуализации части виртуального про- странства рисования. Перед началом рисования не помещающиеся в него фраг- менты изображения отсекаются. Использование набора инструментов рисования На рис. 4.2 представлены состав и последовательность работы с набором ин- струментов рисования. В Windows он называется контекстом устройства. В на- бор входят такие инструменты, как перо, кисть и шрифт. Для работы с набором инструментов нужно придерживаться определенного протокола, то есть последовательности операций. СОВЕТ Идея набора инструментов рисования не уникальна для Windows. Впервые она появилась в системе X Window - оконной системе для ОС Unix. В X Window набор инструментов называется графическим контекстом, ему соответствует тип дан- ных GC. Для работы с графическим контекстом в X Window применяется пример- но такой же протокол, как в Windows. Следуя взглядом по рис. 4.2 слева направо, мы видим следующие шаги: Изменить Новый Старый Захватить Объект набора инструментов Перо Кисть Шрифт Рисовать Восстановить Старый инструмент Текущий набор Наборинструментов Рис. 4.2. Работа с набором инструментов 1. Захватить контекст устройства. В захваченном контексте имеются стан- дартные инструменты, определяемые Windows GDI. Как минимум набор инструментов содержит перо, кисть и шрифт. Перо нужно для рисования линий, кисть - для закрашивания областей, а шрифт - для вывода текста. В набор входят и другие инструменты, но здесь мы и' > не будем
Рисование изображений 11ИВНЯНМ 93 2. Изменить контекст устройства, поместив в него новые инструменты. Иногда стандартного набора достаточно. Но чаще программа должна вре- менно подменить некоторые инструменты. Например, по умолчанию обычно предоставляется черное перо, а программа хочет нарисовать ло- маную линию красным цветом. Для этого нужно сначала создать красное перо, а потом поместить его в контекст устройства. При такой замене ин- струментов GDI возвращает старый инструмент (в данном случае черное перо). Программа может запомнить его для последующего использо- вания. 3. Выполнить операции рисования с использованием текущего контекста устройства. Когда программа хочет выполнить любую операцию рисова- ния, например провести линию, она передает соответствующей функции текущий контекст устройства. Компонент GDI выбирает из него подходя- щий инструмент. 4. Восстановить начальное состояние контекста устройства. Закончив ри- сование, программа должна вернуть в контекст устройства исходные ин- струменты, то есть восстановить начальное состояние для следующего пользователя. Для этого следует запоминать описатели инструментов, воз- вращенные GDI при изменении контекста, а в конце поместить их в кон- текст. ПРИМЕЧАНИЕ Контекст устройства - это разделяемый между приложениями ресурс GDI. Если программа не восстановит исходное состояние контекста, то следующий пользо- ватель получит его в непредсказуемом состоянии. В результате программа мо- жет начать рисовать красным пером, хотя и не запрашивала его. Использование контекста устройства в качестве набора инструментов облада- ет рядом преимуществ. Они с лихвой компенсируют дополнительные усилия для следования описанному протоколу. Главное достоинство - простота вызова функ- ций рисования. Первым аргументом любой такой функции всегда является кон- текст устройства. Нам не нужно указывать конкретный инструмент рисования и его атрибуты, все это уже хранится в контексте. Кроме того, контекст устройства позволяет приложению считать инструменты рисования виртуальными сущнос- тями, которые не зависят от физического оборудования. Поэтому программа ком- пилируется и исполняется правильно, какими бы возможностями ни располагала аппаратура вывода на экран. Имеющиеся стили пера и кисти Основными инструментами рисования в нашей анимационной программе являются перо и кисть. Перо служит для рисования линий, а кисть - для закраши- вания (заливки) областей. На рис. 4.3 представлен весь диапазон стилей перьев и иистрп ггпрппгтАШТЯС?' idowx QDT
94 Illi Обзор платформы Pocket PC Рассмотрим сначала перья. Как видите, GDI позволяет программе управлять тремя характеристиками пера: стилем, цветом и шириной. Стиль определяет спо- соб проведения линии: Solid (сплошная), Dash (штриховая), DashDot (штрих- пунктирная), DashDotDot (штрих-пунктир-пунктирная) и Dot (пунктирная). На рисунке показаны примеры применения каждого стиля. Помимо стиля, програм- ма может задать цвет пера, а также его ширину в пикселях. Последний атрибут позволяет рисовать тонкие или жирные линии. Доступные приложению стили кистей также показаны на рис. 4.3. В GDI есть три вида перьев: сплошные, шаблонные и штриховые. Когда выбрана сплошная кисть, программа закрашивает фигуру, например прямоугольник одним цветом. Шаблонная кисть повторяет заданное растровое изображение, а штриховая - за- полняет фигуру линиями, отстоящими друг от друга на некоторое расстояние. Одновременно с заданием стиля кисти можно задать и ее цвет. В случае штрихо- вой кисти линии рисуются указанным цветом. Для штриховых костей можно за- дать также расстояние между линиями в пикселях. Пользуясь этими возможнос- тями в различных сочетаниях, приложение для Pocket PC может довольно гибко выполнять рисование. Инструмент Стиль #5 Перо Сплошная--------------------- Штриховая-------------------- Штрих-пунктирная-------------------- Штрих-пунктир-пунктирная--------------------’ Пунктирная .................. i ^Произвольный цвет Штриховая Шаблонная Обратно- диагональная Крестовая Диагонально- крестовая Г оризонтальная Вертикальная ^Произвольный цвет | Рис. 4.3. Стили перьев и кистей
Рисование изображений iiiinani 95 Операции рисования Поместив в контекст устройства перо и кисть нужного стиля, приложение мо- жет приступать к рисованию. На рис. 4.4 показаны различные операции рисова- ния, предоставляемые GDI. Каждой операции нужно знать контекст устройства и начальную либо конеч- ную точку. Поэтому иногда говорят о векторных операциях рисования. Оконеч- ные точки (вектор) и тип графического объекта, например прямая линия, - это параметры алгоритма рисования. Рассмотрим, к примеру, команду рисования прямоугольника. Ей передаются координаты левого верхнего и правого нижнего углов в виртуальном простран- стве рисования. Это пространство состоит из очень большого числа пикселей, и приложение может произвольно задать положение прямоугольника в нем. Но из- за отсечения прямоугольник может и не появиться на экране. Операции отсече- ния мы рассмотрим в следующем разделе. [ Операции | Графическое изображение | Настольный ПК | | Pocket PC | Прямоугольник Да Да Эллипс CUD Да Да Отрезок прямой _ Да Да Многоугольник Да Да Скругленный прямоугольник Да Да Сектор Да Нет Дуга Да Нет Кривая Безье Да Нет Рис. 4.4. Операции рисования, поддерживаемые GDI СОВЕТ ______________ Диапазон координат по осям X и Y в виртуальном пространстве рисования очень велик. Функции рисования принимают в качестве аргументов 32-разрядные це- лые со знаком. Это означает, что обе координаты могут принимать значения от - 2*31 + 1 до+2п 31 -1. Благодаря этому в клиентской области можно создавать изображения, намного превосходящие размер физического экрана. Описанная ниже методика отсечения применяется для отображения на экране части клиент- ской области.
96 Ill Обзор платформы Pocket PC На рис. 4.4 видно, что объем поддержки со стороны GDI различен для на- стольных ПК и Pocket PC. На платформе Windows СЕ число графических прими- тивов меньше, не реализовано рисование секторов, дуг и кривых Безье. ' 'ЖМ-тН СОВЕТ *1 ' В книгах, посвященных машинной графике, приводятся алгоритмы рисования. Они аппроксимируют кривые линии большим числом отрезков прямых. Посколь- J куна Pocket PC операция рисования отрезка поддерживается, то при желании можно реализовать отсутствующие алгоритмы самостоятельно. Отметим, что в списке нет операции для рисования окружности. Но это не страшно. Ведь окружность - просто частный случай эллипса, для которого охва- тывающим прямоугольником служит квадрат. Поскольку любой операции рисования передается контекст устройства, то программа может рисовать указанные фигуры разными перьями и цветами, с гра- ницами разной толщины. Для закрашивания внутренних частей фигуры приме- няются кисти. Операции отсечения Windows GDI позволяет приложению управлять тем, какая часть полного изображения будет видна в клиентской области. Этот механизм называется про- кручиваемым окном, хотя более правильно называть его окном отсечения. Любая Windows-программа рисует изображение в виртуальном пространстве, применяя команды, показанные на рис. 4.4. Та часть изображения, которая попа- дает в окно отсечения, видна в клиентской области. Наоборот, если некоторая часть изображения оказывается за пределами окна отсечения, то она не видна в клиентской области. Другими словами, окно отсечения вырезает часть изобра- жения. Виртуальная система координат в Windows GDI двумерна. Ось X в ней распо- ложена горизонтально, а ось Y - вертикально. Первоначально верхний левый угол окна отсечения в виртуальном простран- стве совпадает с верхним левым углом клиентской области окна приложения. В процессе отображения GDI отсекает все части изображения, не попавшие в окно отсечения. В случае настольного ПК у программы есть возможность пере- мещать окно отсечения, открывая одни и скрывая другие части изображения. За счет перемещения окна отсечения можно добиться эффекта анимации, как показано на рис. 4.5. Вследствие перемещения окна отсечения относительно начала координат виртуального пространства графический объект оказывается в разных частях клиентской области. Следовательно, программа может анимировать любое изоб- ражение, просто сдвигая окно отсечения. На рис. 4.5 эта техника анимации иллюстрируется сдвигом левого верхнего угла окна отсечения. Первоначально окно отсечения занимает о' одное полож
Рисование изображений IIIIIW 97 ние - его левый верхний угол совпадает с левым верхним углом виртуального пространства. Если переместить его в точку (-10, +10), то мы увидим другую часть виртуального пространства. А стало быть, отрезок окажется в другой части клиентской области. До перемещения начала координат окна После перемещения начала координат окна в точку (-10, -10) [Виртуальная система координат! [Виртуальная система координат] Рис. 4.5. Отсечение в версиях Windows для настольного ПК Для перемещения окна отсечения достаточно одной команды. Всего одна ко- манда - и графический объект анимирован! Неплохо. Но Windows СЕ не поддерживает отсечения в виртуальном пространстве и этим сильно отличается от версий Windows для настольного ПК. Программа для Windows СЕ перемещает само изображение, а не окно отсечения. Перемещение сложного изображения, для создания которого нужно выполнить много операций рисования, интенсивно потребляет вычислительные ресурсы. Поэтому пользова- тель наблюдает большие задержки, и такая «ручная» анимация выглядит менее плавно. ПРИМЕЧАНИЕ Для анимации изображения вручную нужно выполнить несколько действий. Сна- чала программа определяет смещение от начала координат виртуального про- странства. Затем это смещение прибавляется к аргументам каждой операции рисования. После чего все операции повторяются с новыми значениями аргу-
98 III! Обзор платформы Pocket PC Вывод изображения Изображение появляется на экране в результате выполнения процедуры про- рисовки (rendering). Иногда ее называют также видеоконвейером (viewing pipe- line). Схематично этот конве"йер показан на рис. 4.6. рисования GDI Рис. 4.6. Схематичное представление видеоконвейера Любая команда рисования поступает на конвейер. Обычно GDI кэширует ко- манды рисования, пока их не накопится достаточно много. Затем все операции подаются на вход конвейера одним пакетом, за счет чего уменьшаются накладные расходы, связанные с доступом к драйверу устройства. При выполнении любой команды рисования программа указывает вид опера- ции (например, «нарисовать прямоугольник»), контекст устройства и координа- ты в виртуальном пространстве. Функции Win32 API передают эти команды GDI, чтобы он инициировал работу конвейера. GDI выполняет отсечение, накладывая на результаты рисования окно отсечения. Затем GDI консультируется с драйве- ром, чтобы сопоставить инструменты, хранящиеся в контексте устройства, с тем, что реально может поддержать драйвер. ПРИМЕЧАНИЕ Инструменты рисования в контексте устройства - не более чем логические абст- ракции. Процедура согласования между GDI и драйвером устройства наделяет их физическими визуальными характеристиками. В некотором смысле описанный подход позволяет достичь аппаратной неза- висимости. Если видеоаппаратура и драйвер устройства изменятся, то Windows- программа все равно будет работать, правда, внешне изображение может выгля- деть по-другому. Если видеокарта не поддерживает какого-то цвета, то драйвер подберет наиболее близкий к нему. Линия, которая на одном компьютере выгля- дела красной, на другом может оказаться черной, если второй компьютер обору- дован монохромным дисплеем. СОВЕТ ................................................ ...... Такая ситуация для КПК весьма вероятна. Некоторые устройства поддерживают весь спектр цветов, тогда как другие оборудуются монохромными дисплеями. Цветной дисплей лучше смотрится при слабом освещении или в темноте, а моно-
Рисование изображений illllMH 99 хромный - при ярком солнечном свете. В зависимости от ожидаемого примене- ния программу нужно проектировать с учетом этих особенностей дисплея. Поставляемый изготовителем оборудования драйвер видеоустройства знает все о его аппаратных особенностях. Именно драйвер транслирует команды рисо- вания и согласованные инструменты в последовательность аппаратных команд для заполнения буфера кадров данными, которые должны быть показаны в кли- ентской области. Принудительная перерисовка окна приложения Вследствие действий пользователя у программы может возникнуть необходи- мость перерисовать клиентскую область своего окна. Механизм принудительной перерисовки показан на рис. 4.7. Обычно перерисовка производится в ответ на событие, инициированное си- стемой или пользователем. Термин «модель рисования в Windows» отражает именно этот подход к обновлению окна программы. Модель рисования отделяет управление данными от обновления окна. В результате, чтобы приложение обно- вило свое окно, может потребоваться несколько событий, инициированных систе- мой или пользователем. Вместе с событием приложение получает характеризующие его атрибуты. В качестве примера приведем событие щелчка левой кнопкой мыши в клиентской области; сообщение о нем содержит координаты курсора мыши в момент щелчка. Обработчик сообщения извлекает атрибуты из параметров wParam и iParam и за- поминает их в переменных внутри оконной процедуры или передает для хране- ния менеджеру глобальных данных (помещает в буфер). Затем приложение отда- ет команду перерисовать клиентскую область. В конечном итоге эта команда приведет к вызову обработчика события WM_PAINT, который воспользуется ра- нее запомненными данными для обновления клиентской области. Произошло событие, инициированное системой или пользователем В окно выведено изображение [Обработчик ( события Принудительная перерисовка Сохранить данные Параметры Обработчик сообщения WM_PAINT Рисовать с использованием сохраненных данных Обновить данные, хранящиеся в буфере Риг* А 7 Пг>мн\/ЛИТелЬНая ПРПРпмтд^я к’пмАнтркттй области
100 Illi Обзор платформы Pocket PC В анимационной программе, которую мы собираемся разработать, этот под- ход используется специфическим образом. Наша цель - ограничиться перерисов- кой только той части клиентской области, которая необходима для стирания ста- рого и рисования нового изображения. Тогда анимация будет происходить плавно и без рывков. Изображение обновляется в ответ на регулярные срабатывания таймера. В обработчике сообщений от таймера нужно выполнить следующие действия. 1. Вычислить прямоугольник, охватывающий область, занятую старым изоб- ражением. 2. Стереть часть клиентской области, ограниченную этим прямоугольником. 3. Вычислить прямоугольник, охватывающий область, занятую новым изоб- ражением. 4. Сохранить координаты этого прямоугольника в локальных переменных оконной процедуры. 5. Перерисовать часть клиентской области, ограниченную новым прямо- угольником, изобразив в ней фигуру человечка. В обработчике события рисования нужно выполнить следующие действия. 1. Извлечь координаты нового прямоугольника из локальных переменных, в которых они были запомнены. 2. Нарисовать в этом прямоугольнике фигуру человечка. Благодаря тому что перерисовываются только старая и новая области, фигура перемещается по экрану плавно. Использование таймеров Выше уже отмечалось, что таймеры играют важную роль в любой анимацион- ной программе, да и во многих других приложениях. Таймер сигнализирует об истечении некоторого промежутка времени. В программах слежения за аппарату- рой событие таймера извещает о наступлении момента очередного опроса обору- дования. Приложение-планировщик по сообщению от таймера проверяет, не на- стало ли время запланированного события. Программа (не только для Windows), в которой применяются таймеры, долж- на быть устроена, как показано на рис. 4.8. На этом рисунке представлена диаграмма перехода состояний, на которой изображены состояния программы и переходы между ними при возникновении события таймера. Состояния обозначены помеченными прямоугольниками. Состояние - это память обо всем том, что происходило с приложением до текущего момента. Стрелкой обозначается переход между состояниями. Метка, сопровождающая стрелку, несет важную информацию. Над чертой указывается событие, вызвав- шее переход, а под чертой - описание действия, предпринятого в результате пере- хода. В начале работы программа находится в состоянии «бездействие». Далее ка- кое-то событие, инициированное системой или пользователем заставляет про-
Рисование изображений 101 грамму запустить таймер на заданный интервал времени. Следовательно, «Собы- тие создания» возникает в состоянии бездействия. В ответ приложение выполня- ет действие «Запустить таймер». Теперь программа перешла в состояние «Ожи- дание таймера». <Событие таймера> (Вызвать OnTimer) <Событие уничтожения> (Остановить таймер) Рис. 4.8. Использование таймера Через регулярные промежутки времени таймер срабатывает, и возникает «Событие таймера». Поскольку в этот момент программа находится в состоянии «Ожидание таймера», то выполняется действие «Вызвать OnTimer». OnTimer - это обработчик события таймера, реализованный в оконной процедуре. Затем программа снова возвращается в состояние «Ожидание таймера». Рано или поздно от пользователя или от системы придет сообщение, требующее уничтожить таймер. На рис. 4.8 оно названо «Событием уничтожения». В ответ приложение выполняет действие «Остановить таймер» и возвращается в состояние бездействия. В нашей анимационной программе эта общая последовательность приобрета- ет такую конкретную форму. □ Состояние бездействия. Промежуток времени до создания окна приложе- ния. □ Событие создания. Окно приложения получает сообщение WM_CREATE. □ Запустить таймер. Запускает таймер, задав время срабатывания. □ Событие таймера. Через заданные интервалы поступают сообщения WM TIMER.
102 Illi Обзор платформы Pocket PC □ Вызвать OnTimer. Реализация обработчика сообщения OnTimer. □ Событие уничтожения. Пользователь решает выйти из программы. □ Остановить таймер. Останавливает таймер, после чего события от него не поступают. В обработчике сообщений от таймера программа выполняет действия, застав- ляющие фигурку двигаться. Мы уже говорили, что для этого нужно стереть ста- рую фигурку, вычислить положение новой и принудительно перерисовать окно. Применение инкапсуляции в проекте приложения Эта книга посвящена прежде всего выработке правильных подходов к Проек- тированию. В предыдущих главах было много сказано о преимуществах, которые дает удачное структурирование программы. В этом разделе мы приведем пример конкретной структуры - инкапсуляцию. Цель инкапсуляции - скрыть детали обработки, обеспечив подходящий ин- терфейс. Программа может инкапсулировать разделяемые глобальные данные, предоставив методы доступа к ним. Другим примером инкапсуляции служит со- крытие группы логически связанных операций за набором функций. У техники сокрытия деталей реализации немало достоинств. Программы, которые эффек- тивно пользуются этим приемом, быстрее писать и легче отлаживать. К тому же они более производительны и проще расширяются. Сокращение времени реализации обусловлено тем, что разработчик может снова и снова использовать проверенный инкапсулированный код, не тратя вре- мени на написание и отладку нового. Будучи один раз написаны и отлажены, ин- капсулированные функции, как кирпичи, образуют фундамент, на котором мож- но возвести всю конструкцию. Путем обращения к этим надежным, как скала, функциям можно реализовать новые возможности. Если инкапсуляция применяется эффективно, отладка становится совсем простым делом. Поскольку детали реализации инкапсулированы в функции, то во время отладки их можно отделить от остальной программы. Если ошибка при- таилась в инкапсулированной функции, то только ее и нужно отладить. Кроме того, вызовы функций позволяют четко проследить логику программы, так что все ее изъяны становятся очевидны, и это тоже упрощает процесс отладки. Хоро- шо организованная инкапсуляция дает возможность читать программу на С или C++ почти как обычный англоязычный текст. Это способствует быстрому обна- ружению логических ошибок. Обращение к уже реализованным функциям уменьшает объем потребляемой программой памяти. Многие разработчики просто копируют куски кода, а затем меняют имена переменных, вместо того чтобы инкапсулировать этот код в функ- цию, принимающую аргументы. Такой подход ведет к разбуханию кода, излишне- му потреблению памяти и уменьшению скорости реакции приложения на дейст- вия пользователя. Применение же функций с аргументами оставляет в программе только один фрагмент кода, что сокращает размер программы.
Применение инкапсуляции !!!! 103 СОВЕТ ________________________________________— Ниже в разделе «Анализ эффективности инкапсуляции» мы приведем пример раз- бухания кода, возникающего из-за ненадлежащего применения инкапсуляции. Если в проекте программы широко используется инкапсуляция, то добиться расширяемости обычно не составляет труда. Добавление новых функций не отра- жается ни на тех, что были инкапсулированы ранее, ни на работе остальных час- тей программы. Расширение приложения происходит путем добавления новых или существующих инкапсулированных функций, которые уже были отлажены. Как правило, для этого не приходится прилагать много времени или усилий. В этой главе мы воспользовались предоставленной возможностью создать на- бор инкапсулированных, повторно используемых функций для программы ани- мации. Напомним, что изменение контекста устройства должно следовать опре- деленному протоколу. Смысл его в том, что перед возвратом контекста системе необходимо восстанавливать исходное состояние всех модифицированных ин- струментов. Вот этот протокол и можно инкапсулировать. Рассмотрим объявление такой функции: void DrawLineShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line_style, int line_color); Она скрывает детали рисования отрезка прямой, соединяющей две точки в виртуальном пространстве, позволяя задать произвольную ширину, стиль и цвет прямой. В качестве аргументов эта функция принимает контекст устройства DC, на- чальную точку (xl, yl), конечную точку (х2, у2) и дополнительные параметры, описывающие характеристики прямой. Разумеется, начальная и конечная точки заданы виртуальными координатами, а значит, часть отрезка может быть отсечена GDI. Первый дополнительный параметр, line_width, задает ширину линии в пик- селях. Следующий параметр line_style описывает один из стилей, показанных на рис. 4.4, и может принимать значения PS_SOLID, PS_DOT, PS_DASH и PSD ASHDOT. Параметр line_color задает сочетание красного, зеленого и синего цветов с помощью макроса RGB, определенного в файле windows.h. Вот как можно обратиться к этой инкапсулированной функции из программы: DrawLineShapeAt(DeviceContext, 0, 0, 75, 75, 1, PS_SOLID, RGB(0,255,0) ); В результате будет нарисован отрезок прямой, соединяющий начало (0,0) с точкой (5,75) в виртуальной системе координат. Это будет сплошная зеленая линия шириной 1 пиксель. Мы воспользовались для задания цвета макросом RGB(0,255,0). Если быть точным, этот макрос задает смесь цветов в виде В.СВ(ДоляКрасного,ДоляЗеленого, ДоляСинего). Буквы в названии макроса напоминают, в каком порядке нужно пе- речислять цвета в списке аргументов (Red - красный, Green - зеленый, Blue - синий). Доля каждого цвета может изменяться от 0 до 255. Если значение равно 255, значит, r ланное сочетание включена максимально возможная поля cootrptctrvki-
104 НИ Обзор платформы Pocket PC щего цвета. Показанное выше сочетание включает только зеленый цвет, причем доля его максимальна. Поскольку каждое значение изменяется от 0 до 255, то для представления доли одного цвета нужно 8 битов. Макрос RGB объединяет три 8-битовых числа в одно 32-битовое значение, которое видеодрайвер использует для управления аппаратурой. Некоторым инкапсулированным функциям, например DrawRectangleShapeAt, необходим еще аргумент fill_brush. Он может принимать значения, соответствую- щие одной из готовых кистей, а именно: BLACK_BRUSH, DKGRAY_BRUSH, GRAY_BRUSH, LTGRAY_BRUSH или WHITE_BRUSH. Все эти константы оп- ределены в файле windows.h. Все шаги протокола работы с контекстом устройства реализованы внутри. Функция создает необходимый инструмент рисования, сохраняет его в контексте устройства, выдает команду рисования, после чего восстанавливает исходное со- стояние контекста. Для рисования сложного изображения нужно написать такую последовательность обращений к инкапсулированным функциям, в которой лег- ко разобраться с первого взгляда. СОВЕТ Подробно код этой инкапсулированной функции рассматривается в разделе «Анализ компонента DrawOps» ниже. Компонент проекта, в котором реализована эта функция, предоставляет так- же функции для рисования других фигур: прямоугольника, скругленного прямо- угольника и эллипса. Этот компонент называется DrawOps. В заголовочном фай- ле DrawOps.h перечислены все предоставляемые функции: у*********************************************** * File: DrawOps.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***************************************,*******/ #include <windows.h> #include <windowsx.h> void DrawLineShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line_style, int line_color) ; void DrawRectangleShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line_style, int line_color, int fill_brush) ; void DrawRoundRectShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line_style, int line_color, int fill_brush) ; void DrawEllipseShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line_style
Реализация анимационной программы SlIIIM 105 int line_color, int fill_brush) ; Интерпретация аргументов двух последних методов DrawRoundRectShapeAt и DrawEllipseShapeAt несколько отличается. Эти аргументы описывают прямо- угольник, охватывающий рисуемую фигуру. Мы уже отмечали, что если прямо- угольник, охватывающий эллипс, оказывается квадратом, то эллипс становится окружностью. Реализация простой анимационной программы В этой главе и далее обсуждение всех примеров начинается с диалоговой про- цедуры. Мы уже говорили, что функция WinMain во всех программах одна и та же, поэтому рассматривать ее снова и снова нет смысла. Вот полный исходный текст функции DlgProc для простой анимационной программы: /***** ****,**,**.*****,,**„,**,*******,**„**** * File: DlgProc.с * * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ***********************************************/ ♦include <windows.h> ♦include <windowsx.h> ♦include "resource.h" ♦include "windowsy.h" BOOL OnlnitDialog( HWND hDlg, HWND hDlgFocus, long UnitParam ) void void void DlgOnCommand( HWND hDlg, int UINT uCodeNotify ) ; DlgOnPaint(HWND hDlg) ; DlgOnMove( HWND hDlg, int x, ilD, HWND int у ) ; hDlgCtl, DWORD DlgOnCtlColorStatic( HWND hDlg UINT msgCode ) ; ♦include <tchar.h> ♦include "IFiles.h" ♦include "DataMgr.h" ♦include "PortabilityUtils.h" ♦include "DrawOps.h" , HDC hDC, HWND hDlgChild, ♦define HORIZONTAL_DELTA 100 ♦define VERTICAL_DELTA 0 ♦define HORIZONTAL_MAX 400 ♦define VERTICAL_MAX +400 ♦define ID_TIMER 1 Fi n о TTWP ТМГ'ОГМГМТ
Обзор платформы Pocket PC 106 static int CurrentOriginX = 0 ; static int CurrentOriginY = 0 ; void DlgOnTimer(HWND hDlg, UINT id); BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) { switch (message) { HANDLE—DLG_MSG( hDlg, WM_INITDIALOG, OnlnitDialog ) ; HANDLE—DLG_MSG( hDlg, WM_COMMAND, DlgOnCommand ) ; HANDLE_DLG_MSG( hDlg, WM_PAINT, DlgOnPaint ) ; HANDLE—DLG_MSG( hDlg, WM_M0VE, DlgOnMove ) ; HANDLE_DLG_MSG( hDlg, WM_CTLCOLORSTATIC, DlgOnCtlColorStatic ) ; HANDLE_DLG_MSG( hDlg, WM_TIMER, DlgOnTimer); } return FALSE ; Анализ функции DlgProc Большая часть этого кода вам уже знакома. Поэтому мы обсудим только но- вые элементы. ♦include "DrawOps.h" Этот заголовочный файл дает доступ к инкапсулированным функциям рисо- вания. Они вызываются из обработчика сообщения WM_PAINT, который мы рас- смотрим ниже. ♦ define HORIZONTAL-DELTA 100 ♦ define VERTICAL-DELTA 0 При каждом срабатывании таймера начало области, охватывающей фигуру, смещается по горизонтали и вертикали на указанные величины. Положительное смещение по горизонтали говорит о том, что фигура движется вправо. А посколь- ку VERTICAL_DELTA равно 0, то смещения по вертикали не происходит вовсе. При положительном смещении по вертикали фигура двигалась бы вверх, а при отрицательном - вниз. ♦ define HORIZONTAL—МАХ 400 ♦ define VERTICAL—MAX +400 Если бы не эти ограничения, то рано или поздно фигура исчезла бы из клиен- тской области. А так при достижении максимального значения соответствующей координаты программа возвращает фигуру в исходное положение. ♦ define ID_TIMER 1 ♦ define TIMER-INCREMENT 250 При создании таймера надо задать два параметра. Уникальный целочислен- ный идентификатор ID_TIMER ассоциирует таймер с конкретным окном при- ложения, а величина TIMER-INCREMENT - это число миллисекунд между по- следовательными срабатываниями. В документации по Windows написано, что интервал должен быть не меньше 50 миллисекунд. В противном случае Windows подставит вместо него минимальное допустимое значение.
107 Реализация анимационной программы IIIIIIB static int CurrentOriginX = 0 ; static int CurrentOriginY = 0 ; В этих статических переменных запоминается текущее положение прямо- угольника, охватывающего фигуру человечка. По определению, положение пря- моугольника определяется координатами его левого верхнего угла. В обработчи- ке сообщения WM_TIMER к этим значениям прибавляются определенные выше приращения. Если после увеличения оказывается превышено пороговое значе- ние, то охватывающий прямоугольник возвращается в начало координат. void DlgOnTimer(HWND hDlg, UINT id); Здесь объявлена сигнатура обработчика сообщения WM_TIMER. Эта функ- ция принимает два аргумента: hDlg определяет окно, владеющее таймером, a id описывает таймер, от которого пришло сообщение. Правильно написанный обра- ботчик сообщения WM_TIMER должен сравнить это значение с определенной выше константой ID_TIMER. HANDLE_DLG_MSG( hDlg, WM_TIMER, DlgOnTimer); Этот анализатор, включенный в предложение switch, передает сообщение WM_TIMER обработчику DlgOnTimer. Анализ обработчиков сообщений Во время исполнения обработчики сообщений вызываются в порядке, кото- рый определяется логикой работы программы: 1. Программа запускается, поступает сообщение WM_CREATE, которое пе- редается обработчику OnlnitDialog. 2. OnlnitDialog запускает таймер. При каждом срабатывании таймера програм- ма получает сообщение WM TIMER и вызывает функцию DlgOnTimer. 3. DlgOnTimer стирает старую фигуру и рисует новую. Та и другая операции посылают сообщение WM PAINT, при получении которого вызывается обработчик DlgOnPaint. 4. Когда пользователь хочет выйти из программы и выбирает пункт меню Quit, программа получает сообщение WM_COMMAND и вызывает функ- цию DlgOnCommand, которая останавливает таймер. ПРИМЕЧАНИЕ На самом деле сообщение WM_TIMER никогда не попадает в очередь первичного потока. Когда срабатывает аппаратный таймер, Windows поднимает булевский флаг, извещающий о том, что произошло событие таймера. Внутри цикла выборки сообще- ний в функции DialogBox есть обращение к функции GetMessage, которая проверяет этот флаг. Если он поднят, то GetMessage возвращает сообщение WMJIMER. Ниже мы рассматриваем обработчики событий в описанном порядке, имити- руя реальную последовательность событий. Так проще разобраться в работе про- граммы и понять, как р ’ теле выполняется анимация.
108 Illi Обзор платформы Pocket PC BOOL OnlnitDialog ( HWND hDlg , HWND hDlgFocus , long UnitParam ) { // Рассмотренные ранее строки опущены SetTimer(hDlg,ID_TIMER,TIMER_INCREMENT,NULL) ; return TRUE ; } Помимо рассмотренного выше обязательного начального кода, этот обработчик еще запускает таймер, вызывая функцию SetTimer. Ей передается описатель окна, владеющего таймером, и уникальный целочисленный идентификатор ID_TIMER, описывающий конкретный таймер. Будучи запущен, таймер генерирует события всякий раз по истечении промежутка времени TIMER_INCREMENT. Последний аргумент позволяет сказать системе, что вместо отправки сообщения WM_TIMER нужно вызвать указанную функцию. Если этот аргумент равен NULL (как в нашем случае), то отправляется сообщение. ПРИМЕЧАНИЕ Величина TIMER INCREMENT - это минимальное время между последователь- ными сообщениями WM_TIMER. Если в очереди потока уже есть сообщение WMJIMER, то система не отправляет нового, пока старое не будет извлечено. void DlgOnTimer(HWND hDlg, UINT id) { RECT OldArea ; if ( id == IDJTIMER ) { OldArea.left = CurrentOriginX + 50 ; OldArea.top = CurrentOriginY + 50 ; OldArea.right = CurrentOriginX + 100 ; OldArea.bottom = CurrentOriginY + 215; // Стереть старое изображение InvalidateRect(hDlg,&OldArea,FALSE) ; UpdateWindow(hDlg) ; CurrentOriginX = CurrentOriginX + HORIZONTAL_DELTA ; CurrentOriginY = CurrentOriginY + VERTICAL_DELTA ; if ( CurrentOriginX > HORIZONTAL_MAX ) CurrentOriginX = 0 ; if ( CurrentOriginY > VERTICAL_MAX ) CurrentOriginY = 0 ; // Нарисовать новое изображение InvalidateRect(hDlg,NULL,FALSE) ; UpdateWindow(hDlg) ; } } Обработчик сообщения WM_TIMER стирает старую фигуру, изменяет на- чальное положение охватывающего прямоугольника и рисует фигуру в новом месте.
Реализация анимационной программы 109 if ( id == ID_TIMER ) С окном может быть ассоциировано несколько таймеров, каждый со своим идентификатором и интервалом. Хотя существует только один аппаратный тай- мер, Windows управляет им таким образом, что события таймера поступают в за- прошенные моменты времени. Поскольку все эти события направляются одному и тому же обработчику DlgOnTimer, то необходимо сравнить аргумент id с иден- тификаторами всех известных таймеров, чтобы определить источник и адекватно отреагировать на событие. Константа ID_TIMER определена в теле DlgProc, мы уже рассматривали ее. OldArea.left = CurrentOriginX + 50 ; OldArea.top = CurrentOriginY + 50 ; OldArea.right = CurrentOriginX + 100 ; OldArea.bottom = CurrentOriginY + 215; Чтобы стереть старую фигуру, нужно знать охватывающий ее прямоугольник. В данном фрагменте координаты его вершин помещаются в структуру OldArea типа RECT, объявленную в начале обработчика. Точка (CurrentOriginX, CurrentOriginY) определяет левый верхний угол охватывающего прямоугольни- ка в координатах клиентской области. К ней прибавляются заранее вычисленные длины сторон прямоугольника для вычисления координат правого нижнего угла. InvalidateRect(hDlg, &01dArea, FALSE) ; UpdateWindow(hDlg) ; Эти две строки заставляют программу стереть фигуру, находящуюся в охва- тывающем прямоугольнике OldArea. Функции InvalidateRect передается область OldArea, нуждающаяся в обновлении, и описатель содержащего ее окна. После- дний параметр FALSE говорит, что нужно перерисовать фон лишь в указанной области, что ускоряет обработку и делает анимацию плавной. В результате вызова InvalidateRect в очередь окна hDlg поступает сообщение WM_PAINT. Обращение к функции UpdateWindow перемещает это сообщение в начало очереди, и Windows немедленно вызывает обработчик события рисова- ния, что приводит к стиранию текущей фигуры. ПРИМЕЧАНИЕ Хотя программа ведет себя так, будто в очереди появилось сообщение WM_PAINT, на самом деле такого сообщения не существует. Windows управляет рисованием с помощью специального булевского флага, имеющегося в каждом окне. InvalidateRect поднимает этот флаг, GetMessage проверяет его, a BeginPaint сбрасывает. Функция UpdateWindows на самом деле напрямую вызывает проце- дуру DlgProc, но внешнему наблюдателю кажется, что сообщение WM_PAINTпри- сутствовало в очереди основного потока. CurrentOriginX = CurrentOriginX + HORIZONTAL_DELTA ; CurrentOriginY = CurrentOriginY + VERTICAL_DELTA ; if ( CurrentOriginX > HORIZONTAL_MAX ) CurrentOriginX = 0 ; if ( CurrentOriginY > VERTICAL_MAX ) CurrentOriginY =
Обзор платформы Pocket PC по Назначение этих строк - вычислить начальную точку нового охватывающего прямоугольника. Сначала к координатам текущего начала прибавляются прираще- ния HORIZONTALDELTA и VERTICAL_DELTA Затем новые координаты срав- ниваются с пороговыми значениями HORIZONTAL_MAX и VERTICAL_MAX. Если порог превышен, то соответствующая координата сбрасывается в нуль. InvalidateRect(hDlg,NULL,FALSE) ; UpdateWindow(hDlg) ; В этот момент клиентская область пуста. Показанные выше строки приводят к исполнению обработчика рисования. Передача NULL во втором аргументе гово- рит о том, что обновлению подлежит вся клиентская область. Поскольку третий аргумент равен FALSE, то закрашивание фона подавляется. Итог - быстрая пере- рисовка фигуры в новом месте. ПРИМЕЧАНИЕ Плавность анимации обеспечивается ограничением объема рисования в клиентс- кой облает. При стирании перерисовывается только прямоугольник, охваты- вающий текущую фигуру, а при рисовании новой фигуры - прямоугольник, охваты- вающий ее новое положение. Это достигается заданием подходящих значений последних двух параметров InvalidateRect с последующим вызовом UpdateWindow. void DlgOnPaint(HWND hDlg) { // Рассмотренные ранее строки опущены // Голова DrawEllipseShapeAt(DeviceContext, CurrentOriginX + 50, CurrentOriginY + 50, CurrentOriginX + 100, CurrentOriginY + 100, 1, PS_SOLID, RGB(255,0,0), GRAY_BRUSH) ; // Туловище DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 100, CurrentOriginX + 75, CurrentOriginY + 200, 1, PS_SOLID , RGB(0,255,0) ) ; // Руки DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 150, CurrentOriginX + 50, CurrentOriginY + 135, 1, PS_SOLID , RGB(0,0,255) ) ; DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 150, CurrentOriginX + 100, CurrentOriginY + 135, 1, PS_SOLID , RGB(0,0,255)) ; // Ноги DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 200, CurrentOriginX + 50, CurrentOriginY + 215, 1, PS_SOLID , RGB(0,0,255)) ; DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 200,
111 Реализация анимационной программы IIIHHMHB CurrentOriginX + 100, CurrentOriginY + 215, 1, PS_SOLID , RGB(0,0,255)) ; // Рассмотренные ранее строки опущены } Закрасив фон клиентской области белой кистью, обработчик сообщения далее приступает к рисованию фигуры в новом месте. При этом он пользуется инкапсули- рованными методами из компонента DrawOps, например DrawLineShapeAt. К ра- нее запомненным координатам начальной точки прямоугольника CurrentOriginX и CurrentOriginY прибавляются фиксированные приращения - в результате по- лучается человечек. Разные части тела рисуются разными цветами: голова - тем- но-серым с красным контуром, туловище - зеленым, а руки и ноги - синим. В качестве примера рассмотрим рисование туловища: DrawLineShapeAt(DeviceContext, CurrentOriginX + 75, CurrentOriginY + 100, CurrentOriginX + 75, CurrentOriginY + 200, 1, PS_SOLID , RGB(0,255,0) ) ; Напомним, что два аргумента, следующие за DeviceContext, определяют на- чальную точку отрезка в виртуальном пространстве. Чтобы туловище соприкаса- лось с головой, мы прибавляем к координатам начальной точки прямоугольника смещения (75,100). void DlgOnCommand( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { switch) ilD ) { case IDOK: KillTimer(hDlg, ID_TIMER) ; EndDialog(hDlg, 0) ; break ; } } Перед тем как выйти из приложения, вызвав EndDialog, этот обработчик оста- навливает таймер, обращаясь к функции KillTimer. Ей передаются два аргумента: описатель окна-владельца hDlg и идентификатор таймера ID_TIMER. Анализ компонента DrawOps Этот компонент содержит набор функций, инкапсулирующих отдельные опе- рации рисования. Все функции следуют общему образцу. 1. Создать необходимые инструменты рисования. 2. Поместить созданные инструменты в контекст устройства. 3. Выполнить запрошенную операцию рисования. 4. Восстановить исходные инструменты в контексте устройства. Поскольку логика всюду одна и та же, мы рассмотрим лишь одну функцию - DrawRectangleShapeAt. Вот ее исходный текст: void DrawRectangleShapeAt(HDC DC, int xl, int yl, int x2, int y2, int line_width, int line__style,
Обзор платформы Pocket PC 112 int line_color, int fill_brush) { HPEN newPen ; HPEN oldPen ; HBRUSH newBrush ; HBRUSH oldBrush ; newPen = CreatePen(line_style, line_width, line_color); oldPen = Selectobject(DC, newPen) ; newBrush = GetStockBrush(fill_brush) ; oldBrush = Selectobject(DC, newBrush) ; Rectangle(DC, xl, yl, x2, y2); SelectObject(DC, oldPen); Selectobject(DC, oldBrush) ; DeletePen( newPen ) ; // Никогда не следует удалять готовые объекты } В этой функции создаются инструменты - перья и кисти, которые будут ис- пользованы при рисовании. Поэтому вначале объявляются их описатели: HPEN newPen ; HPEN oldPen ; HBRUSH newBrush ; HBRUSH oldBrush ; При создании нового инструмента GDI заполняет некоторую структуру дан- ных, а приложение, как обычно, получает описатель этой структуры. Описатель является уникальным целым числом, а не указателем, его единственное назначе- ние - быть переданным той или иной функции в качестве аргумента. Тип данных описателя пера - HPEN, описателя кисти - HBRUSH. В переменных, имя которых начинается с new, например newPen, сохраняется описатель нового инструмента. Если имя переменной начинается с old, то в ней хра- нится описатель прежнего инструмента, находившегося в контексте устройства. newPen = CreatePen(line_style, line_width, line_color); oldPen = SelectObject(DC, newPen) ; Функция CreatePen создает новое перо. Ей передаются три аргумента: шири- на пера в пикселях (line_width), стиль пера (line_style) и его цвет (line_color). Стиль может принимать одно из значений, определенных в файле windows.h: PS_SOLID, PS_DOT, PS_DASH и PS_DASHDOT. Цвет задается в виде комби- нации долей красного, зеленого и синего, объединяемых макросом RGB. СОВЕТ Подробное обсуждение макроса RGB см. в разделе «Применение инкапсуляции в проекте приложения» выше. Создав перо, функция сохраняет его в локальной переменной newPen типа HPEN. Затем эта переменная передается функции SelectObject. Та устанавливает
ИЗ Реализация анимационной программы 1IIIIBU новое перо в контекст устройства DC и возвращает описатель пера, который нахо- дился там раньше. Описатель старого пера запоминается в переменной oldPen. newBrush = GetStockBrush(fill_brush) ; oldBrush = SelectObject(DC, newBrush) ; Для получения одной из готовых кистей применяется функция GetStockBrush, у которой есть единственный аргумент - стиль закрашивания. Он может прини- мать одно из предопределенных значений: BLACK BRUSH, DKGRAY_BRUSH, GRAY_BRUSH, LTGRAY_BRUSH или WHITE_BRUSH. Как и CreatePen, функция GetStockBrush возвращает описатель готовой кис- ти, который сохраняется в переменной newBrush. Затем эта кисть устанавливает- ся в контекст устройства функцией SelectObject, а описатель находившейся там ранее кисти запоминается в переменной oldBrush. ГПЖ' Г-Г1 совет прИ вызове SelectObject не нужно указывать вид ресурса, например перо или кисть. Зная описатель, Windows может добраться до соответствующей структуры, ----------- самостоятельно определить вид ресурса и установить его в контекст устройства. Rectangle(DC,xl,yl,x2,у 2); Далее программа рисует прямоугольник, обращаясь к функции Rectangle. Ей передаются контекст устройства DC и координаты углов прямоугольника в вир- туальном пространстве. Поскольку новое перо уже было установлено в контекст устройства, то рисование происходит этим пером. Кроме того, внутренность пря- моугольника закрашивается установленной в контекст кистью. SelectObject(DC, oldPen); SelectObject(DC, oldBrush) ; Назначение этих двух строк - восстановить в контексте устройства исходные перо и кисть. Для этого достаточно вызвать SelectObject, передав ей описатель исходного инструмента. При вызове SelectObject возвращается описатель ин- струмента, находившегося перед этим в контексте устройства, но мы и так их зна- ем - они хранятся соответственно в переменных newPen и newBrush. DeletePen( newPen ) ; По завершении рисования мы удаляем созданное перо, описатель которого хранится в переменной newPen. Для этого следует вызвать функцию DeletePen. При создании новых инструментов, в частности пера и кисти, Windows выделя- ет память из кучи, принадлежащей GDI. Вызов DeletePen освобождает эту память. // Никогда ПРИМЕЧАНИЕ Куча, принадлежащая GDI, имеет фиксированный размер и никогда не растет. Если программа забудет освободить эту память, то рано или поздно куча будет исчерпана, так что создание инструмента завершится с ошибкой. В этом случае рисование будет выполняться некорректно. не следует удалять готовые объекты
114 ПИ Обзор платформы Pocket PC Если программа удалит готовую кисть, то другие приложения столкнутся с проблемами. Обычно это приводит к тому, что при попытке что-то нарисовать программа аварийно завершается. Благодаря механизму изоляции процессов, ре- ализованному в ядре, «падает» только одно приложение, а не система в целом, как было в ранних версиях Windows. Анализ эффективности инкапсуляции Некоторые программисты считают инкапсуляцию излишеством. В этом раз- деле мы продемонстрируем ее полезность. Вернитесь к тексту обработчика сообщения DlgOnPaint. Для рисования фигу- ры человечка нам потребовалось всего шесть строк кода. А теперь посмотрите на инкапсулированные функции. В среднем каждая из них занимает восемь строк. Если бы поместить весь этот код в обработчик DlgOnPaint, его размер вырос бы до ' 48 строк. Хуже того, поскольку пять из шести вызовов приходятся на DrawLine- ShapeAt, то 40 из 48 строк были бы просто слегка модифицированным повторени- ем одного и того же кода. Это громоздко и уродливо. За счет же повторного использования кода, инкапсулированного в функции DrawLineShapeAt, общее число строк, исполняемых для рисования фигуры, рав- но 22 (шесть вызовов функций и по восемь строк в теле каждой функции) вместо 48 (шесть копий кода внутри тела функции плюс восемь строк, принадлежащих самому обработчику). Следовательно, инкапсуляция позволила сократить размер кода на 45% по сравнению с копированием. Для более сложных изображений эко- номия оказалась бы намного больше. Код функции DlgOnPaint прост и понятен. Имена функций точно соответ- ствуют логическим операциям. А представьте, как выглядел бы код, содержащий 48 скопированных строк, - читать невозможно! И при возрастании сложности изображения ситуация только ухудшилась бы. Резюме В этой главе на примере анимации фигуры человечка мы познакомились с ис- пользованием компонента GDI. Для управления анимацией мы воспользовались программным таймером. Компонент DrawOps инкапсулирует протокол работы с инструментами рисования в Windows-приложениях. Вот основные моменты, которые следует запомнить: □ после завершения рисования нужно вернуть контекст устройства в исход- ное состояние; □ приложение рисует в виртуальном пространстве, a GDI производит отсе- чение; □ Windows СЕ поддерживает только подмножество операций рисования, до- ступных в версиях Windows для настольного ПК; □ в Windows СЕ окно отсечения фиксировано и находится в левом верхнем углу виртуального пространства;
Примеры программ в Web 1ННИ 115 □ с окном можно ассоциировать несколько таймеров с разными интервалами; □ для плавной анимации приложение должно ограничивать размеры перери- совываемых областей; □ инкапсуляция позволяет управлять структурами данных и группировать логически связанные функции; □ благодаря эффективному применению инкапсуляции программы удается быстрее разрабатывать, а их отладка упрощается. Они также оказываются производительнее и легче поддаются расширению. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка Простая анимационная программа для настольного ПК SimpleAnimationProgram Простая анимационная программа для Pocket PC SimpleAiimationProgramPPC Инструкции по сборке и запуску Простая анимационная программа для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект SimpleAnimationProgram.dsw в папке SimpleAnimation- Program. 3. Соберите программу. 4. Запустите программу. 5. Фигура человечка должна плавно перемещаться по горизонтали в клиент- ской области без мигания. 6. Выберите пункт меню Quit. 7. Окно закроется, так как приложение завершило работу. 8. Измените значения констант HORIZONTAL_DELTA и VERTICALJDELTA 9. Соберите и запустите программу. 10. Посмотрите, как изменилось движение фигурки. 11. Выберите пункт меню Quit. 12. Окно закроется, так как приложение завершило работу. Простая анимационная программа для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект SimpleAnimationProgramPPC.vcw в папке Simple- AnimationProgramPPC. 7. Соберите программу.
116 Illi Обзор платформы Pocket PC 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу SimpleAnimationProgram. 12. Фигура человечка должна плавно перемещаться по горизонтали в клиен- тской области без мигания. 13. Коснитесь пункта меню Quit стилосом. 14. Окно закроется, так как приложение завершило работу. 15. Измените значения констант HORIZONTAL_DELTA и VERTICAL- DELTA 16. Соберите программу. 17. Убедитесь, что программа успешно загрузилась в КПК. 18. Запустите программу SimpleAnimationProgram. 19. Посмотрите, как изменилось движение фигурки. 20. Коснитесь пункта меню Quit стилосом. 21. Окно закроется, так как приложение завершило работу.
rl Hi Глава 5. Реализация программы рисования В этой главе мы займемся разработкой программы, обладающей рядом возмож- ностей, которые могут оказаться полезны в реальном приложении для Pocket PC. Речь идет о механизме, который встречается в любой программе автоматизированно- го проектирования (CAD) и графическом редакторе, например Microsoft PowerPoint. Для того чтобы продемонстрировать, как использовать новые возможности программирования для Windows, мы будем реализовывать программу по частям. Сначала покажем, как с помощью эластичного контура нарисовать прямую. За- тем модифицируем программу, добавив рисование одной текстовой метки. После этого включим в программу меню, которое позволит выбирать вид фигуры: отре- зок прямой, прямоугольник, скругленный прямоугольник или эллипс. Помимо всего вышеперечисленного, мы остановимся в этой главе на некоторых важных вопросах проектирования. Первый из них - это использование класса, в котором хранятся свойства, необходимые для рисования текущего объекта. Второй - применение этого класса для управления характеристиками рисуемого объекта. Следующий вопрос - разбиение функциональных возможностей на связанные между собой классы, отнесенные к разным уровням. И наконец, за счет применения конечного автомата мы преобразуем запутанный и «корявый» код обработки собы- тий мыши и клавиатуры в элегантную, легко расширяемую программу. ПРИМЕЧАНИЕ _______________________________________________ Тема правильного проектирования красной нитью проходит через всю книгу. Вопросы проектирования следует обсудить и решить на ранних этапах цикла раз- работки. Если заняться ими после того, как уже написан большой объем кода, то для его модификации потребуются значительные усилия и много времени. В этой главе подход к анализу кода будет несколько иным. В первом разделе мы приведем полный исходный текст диалоговой процедуры DlgProc. Он мало чем отличается от минимальной диалоговой программы, представленной в главе 3. Но в последующих разделах листинги становятся слишком длинными, чтобы сто- ило приводить их целиком. Поэтому мы поступим иначе. Код анализируется по мере добавления новых шагов в реализацию и сопровождается обсуждением про- ектных решений. Во многих случаях обсуждается не одна, а сразу несколько строк кода. Мы предполагаем, что вы уже имеете представление о программировании в Windows, поэтому рассматрив * отдельные строки нет необходимости.
118 Illi Реализация программы рисования Рисование объектов с помощью эластичного контура В этом разделе мы модифицируем минимальную диалоговую программу из главы 3, так чтобы пользователь мог рисовать прямую линию. Для этого нам пона- добится эластичный контур. Суть этой техники в том, что пользователь отмечает начальную точку, а затем буксирует мышь, пока она не окажется в желаемой ко- нечной точке. И пока пользователь подыскивает конечную точку, программа ри- сует отрезок, соединяющий начальную точку и текущее положение мыши. СОВЕТ Термин «эластичный контур» - это метафора резинки, закрепленной в начальной точке и в точке местонахождения мыши. Когда пользователь буксирует мышь, ре- зинка растягивается или сжимается и образует отрезок, соединяющий две точки. На рис. 5.1. показаны графический интерфейс программы и инструкции по рисованию линии методом эластичного контура. Цифрами обозначены шаги, вы- полняемые пользователем. Вы видите окончательное положение отрезка, соеди- няющего начальную и конечную точки. 1. Нажать и удерживать левую кнопку мыши 2. Перемещать мышь, не отпуская левой кнопки 3. Отпустить левую кнопку мыши Рис. 5 1. Пользовательский интерфейс программы рисования прямой методом эластичного контура
Рисование объектов IIIIIBHI 119 Пользователь начинает рисовать линию, поместив курсор мыши в начальную точку. Эта точка во время рисования остается неподвижной. Чтобы зафиксиро- вать начальную точку, нажмите и удерживайте левую кнопку мыши. Не отпуская левой кнопки, отбуксируйте мышь в конечную точку. Пока левая кнопка остается нажатой, программа рисует прямую, соединяющую начальную точку с текущим положением курсора мыши. Как только пользователь отпустит кнопку мыши, программа зафиксирует конечную точку и окончательно нарисует прямую. Алгоритм и данные, необходимые для поддержки эластичного контура, пред- ставлены на рис. 5.2. Согласно рисунку, программа должна хранить данные о трех точках. В пере- менной DragStart хранится начальная точка, используемая во всех операциях ри- сования. В любой момент программа также должна знать еще две точки, необ- ходимые для рисования двух разных прямых. Первая описывает последнюю нарисованную прямую и хранится в переменной DragStop. А чтобы нарисовать новую прямую, нужно знать текущее положение курсора, описываемое перемен- ными CurrentX и CurrentY. Переменные DragStart и DragStop имеют тип POINT. Объект типа POINT име- ет два поля: х и у, - содержащие координаты точки в виртуальном пространстве. Рисование эластичного контура состоит из следующих двух шагов. 1. Стереть старый отрезок, соединяющий DragStart и DragStop. 2. Нарисовать новый отрезок, соединяющий DragStart и (CurrentX, CurrentY). 1. Стереть POINT DragStart старый отрезок 2. Нарисовать4^ ''' POINTDragStop новый отрезок (CurrentX, CurrentY) Рис. 5.2. Алгоритм и данные, необходимые для рисования эластичного контура Нарисовать новый отрезок легко с помощью графических операций, описан- ных в главе 4. Напомним, однако, что программа выполняет эти операции в ответ на собы- тия мыши. Последовательность этих событий представлена на рис. 5.3. Когда пользователь приступает к рисованию, нажимая левую кнопку мыши, программе отправляется сообщение WM_LBUTTONDOWN. Получив его, диа- логовая процедура запоминает начальную точку отрезка. По мере буксировки мыши система отправляет сообщения WM_MOUSEMOVE. В ответ диалоговая процедура стирает старый и рисует новый отрезок.
120 Illi Реализация программы рисования СОВЕТ Число отправляемых Windows сообщений типа WM MOUSEMOVE зависит от внут- ренней частоты опроса устройства, которой программа не управляет. Программа лишь обрабатывает все поступающие к ней сообщения WM_M0USEM0VE. К сожалению, сообщения WM_MOUSEMOVE отправляются и тогда, когда пользователь буксирует мышь за пределами окна приложения. Так происходит всегда, а не только во время рисования эластичного контура. Чтобы программа обрабатывала лишь сообщения WM_MOUSEMOVE, приходящие во время рисо- вания контура, мы заводим переменную состояния, различающую два режима: буксировка с рисованием контура и без оного. Рано или поздно пользователь отпустит левую кнопку мыши. В этот момент диалоговая процедура получит сообщение WM_LBUTTONUP. Его обработчик прекратит рисование эластичного контура. Механизмы обработки сообщений, управления данными и рисованием в окне должны работать согласованно, иначе никакого эластичного контура не получит- ся. На рис. 5.4 показано, как все это интегрируется в единое целое. Пользователь буксирует мышь по экрану 1. WM_LBUTTONDOWN 3. WM_LBUTTONUP 2. WM_MOUSEMOVE Рис. 5.3. Последовательность событий мыши во время буксировки 1. Мышь перемещается по экрану 7. В окне рисуется изображение Сохраненные данные 2.(WM_M0USEM0VE 3. CurrentX, CurrentV 4. InvalidateRect, UpdateWindow 8. DragStop 6. DragStart Jwm_paint) Рисовать с использованием сохраненных данных Новое положение курсора сохранено в буфере Рис. 5.4. Обработка сообщений о перемещении мыши во время буксировки
Рисование объектов 121 На рис. 5.4 в восьмиугольниках написаны имена сообщений. Каждая такая метка соответствует вызову обработчика сообщения. Сплошные стрелки обозна- чают запись и чтение данных. Стрелка исходит из источника данных и ведет к получателю. Пунктирными стрелками обозначается исполнение функции. Об- работчик, из которого стрелка исходит, вызывает функцию, а обработчик, в кото- рый стрелка ведет, исполняется в результате того, что функция генерирует неко- торое сообщение. Следуя нумерации шагов на рис. 5.4, мы можем описать порядок действий. 1. Пользователь выполняет буксировку мыши с нажатой левой кнопкой. 2. Во время буксировки генерируются сообщения WM_MOUSEMOVE, ко- торые поступают обработчику события «движение мыши» в диалоговой процедуре. 3. Этот обработчик обновляет координаты точки (CurrentX, CurrentY), ис- пользуя переданные ему аргументы, в которых записано текущее положе- ние курсора мыши. 4. Обработчик события «движение мыши» вызывает функции InvalidateRect и Update Windows. В результате генерируется и тут же обрабатывается со- общение WM_PAINT. 5. Выполняется обработчик события рисования. В результате создается впе- чатление плавного перемещения эластичного контура. 6. Внутри обработчика WM_PAINT переменные DragStart и DragStop опи- сывают старый отрезок. Зная их, обработчик может стереть старый отре- зок. Затем рисуется отрезок, соединяющий точки DragStart и (CurrentX, CurrentY). 7. По выходе из обработчика WM_PAINT в клиентской области появляется новый отрезок. 8. Когда обработчик WM_PAINT возвращает управление обработчику события «движение мыши», последний обновляет значение переменной DragStop, записывая в нее текущее положение курсора. Описанная процедура довольно проста. В ней управление данными отделено от обновления окна. В ответ на сообщение WM_MOUSEMOVE его обработчик обновляет данные и генерирует сообщение WM_PAINT, которое пользуется эти- ми данными для обновления клиентской области окна. СОВЕТ Отделение управления данными от обновления окна - прямое применение собы- тийно-ориентированной модели рисования, которая была описана в главе 1. Для поддержки рисования методом эластичного контура нужно внести в диа- логовую процедуру следующие изменения: 1. Добавить объявления и пустые тела обработчиков сообщений мыши. 2. Объявить статические переменные, необходимые для управления рисова- нием контура.
122 HIM Реализация программы рисования 3. Реализовать обработчики сообщений, в которых выполняется рисование контура. 4. Модифицировать обработчик WM_PAINT, так чтобы он стирал старый и рисовал новый отрезок. Приведенные ниже листинги реализуют эту программу. Добавление объявлений и тел обработчиков сообщений Ниже представлены следующие изменения в диалоговой процедуре: объявле- ния новых обработчиков и новые ветви в предложения switch. Поскольку ниже реализация обработчиков будет подробно рассмотрена, приводить их пустые тела мы не стали. void DlgOnLButtonDown(HWND hDlg, BOOL fDoubleClick, int x, int y, UINT keyFlags); void DlgOnLButtonUp(HWND hDlg, int x, int y, UINT keyFlags); void DlgOnMouseMove(HWND hDlg, int x, int y, UINT keyFlags); BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) { switch (message) { // Строки, рассмотренные выше, опущены HANDLE_DLG_MSG(hDlg,WM_LBUTTONDOWN,DlgOnLButtonDown); HANDLE_DLG_MSG(hDlg,WM_LBUTTONUP,DlgOnLButtonUp); HANDLE_DLG_MSG(hDlg,WM_MOUSEMOVE,DlgOnMouseMove); } return FALSE; } В этом листинге представлены все необходимые обработчики. Сообщение WM_LBUTTONDOWN инициирует рисование эластичного контура в обработ- чике DlgOnLButtonDown. Само рисование производится в обработчике DlgOn- MouseMove сообщения WM_MOUSEMOVE. Когда пользователь отпускает левую кнопку мыши, поступает сообщение WM_LBUTTONUP, которое обрабатывается функцией DlgOnLButtonUp, в результате чего рисование контура прекращается. Чтобы понять, какие данные доступны каждому обработчику, рассмотрим их аргументы. Первым в списке аргументов всегда идет описатель диалогового окна, которому поступило сообщение, поэтому мы обсудим только оставшиеся аргу- менты. void DlgOnLButtonDown(HWND hDlg, BOOL fDoubleClick, int x, int y, UINT keyFlags); Первый новый аргумент - это признак fDoubleClick. Это булевское значение показывает, была ли левая кнопка щелкнута дважды в течение предопределенно- го промежутка времени. Значение TRUE означает двойной щелчок.
Рисование объектов llllIBni 123 СОВЕТ ,^.»<йе!ишма«11ижа.ишйь№ямаш1ИМ»1«1»»«шм1яшд«ммв»ми«дЯ1М11111»ШИ»*1^^ В документации по Windows API не говорится, каково значение промежутка вре- мени, в течение которого фиксируется двойной щелчок. Пара аргументов х, у определяет положение курсора мыши в координатах клиентской области относительно ее левого верхнего угла. Биты в аргументе keyFlags кодируют состояние различных клавиш. Они показывают, какие из кла- виш CTRL и SHIFT были нажаты в момент щелчка левой кнопкой мыши. Имеет- ся набор констант, позволяющих программе проверить отдельные биты в значе- нии этого аргумента. void DXgOnLButtonUp(HWND hDlg, int x, int y, HINT keyFlags); void DlgOnMouseMove(HWND hDlg, int x, int y, HINT keyFlags); Объявления обеих функций не отличаются от объявления функции DlgOnLButtonDown выше. Объявление статических переменных для поддержки буксировки Для поддержки буксировки необходимы переменные двух видов. Во-первых, это переменная состояния, показывающая, происходит в данный момент букси- ровка или нет. А во-вторых, это переменные, описывающие старый и новый отре- зок. Вот полный перечень переменных: static BOOL IsDragging ; static POINT DragStart ; static POINT DragStop ; static int CurrentX ; static int CurrentY ; Все переменные статические. Наличие спецификатора static влечет за собой ряд последствий. Память для этих переменных выделена в статической области. Данные, размещенные в этой области, сохраняют значения на протяжении всего времени работы программы, а, стало быть, доступны всем обработчикам, незави- симо от того, в какой последовательности они выполняются. Сохранение значе- ний между вызовами обработчиков разных сообщений совершенно необходимо для рисования эластичного контура. Спецификатор static также делает перемен- ные видимыми только в пределах данного файла. Впрочем, для рисования конту- ра этот факт не имеет значения. ПРИМЕЧАНИЕ В более сложных программах эти данные должны находиться под управлением менеджера глобальных данных, описанного в главе 3. Но в данном примере они используются только внутри процедуры DlgProc, то есть в одном-единственном контексте, поэтому включение в программу менеджера данных было бы неоправ- данным усложнением.
124 Реализация программы рисования Булевская переменная IsDragging описывает состояние буксировки. Если она равна TRUE, то пользователь буксирует мышь, поэтому должен рисоваться кон- тур. В остальное время (когда пользователь ничего не рисует) переменная равна FALSE. В последнем случае код рисования эластичного контура не исполняется, даже если поступают относящиеся к нему сообщения. Переменные DragStart и DragStop, о которых мы говорили выше, имеют тип POINT. Этот тип определен в Win32 API и состоит всего из двух полей: х и у. С его помощью удобнее объявлять переменные, поскольку он несет информацию об их назначении. Реализация рисования в обработчиках сообщений Последовательность действий, приводящая к рисованию эластичного конту- ра, начинается в обработчике сообщения DlgOnLButtonDown, основное назначе- ние которого - инициализировать соответствующие переменные: void DlgOnLButtonDown(HWND hDlg, BOOL fDoubleClick, int x, int y, UINT keyFlags) { IsDragging = TRUE ; SetCapture(hDlg) ; DragStart.x = x ; DragStart.у = у ; DragStop.x = x ; DragStop.у = у ; 1 Установка флага IsDragging в TRUE активизирует весь код рисования элас- тичного контура. Вызов функции SetCapture из Win32 API говорит Windows, что все последующие события мыши следует посылать окну с описателем hDlg. Та- ким образом гарантируется, что буксировка будет продолжаться, даже если мышь выйдет за пределы окна. После выполнения SetCapture все сообщения WM_MOUSEMOVE доставля- ются диалоговой процедуре, а содержащиеся в них координаты курсора по-пре- жнему вычисляются относительно клиентской области данного окна. Поэтому прямая будет рисоваться из точки DragStart в точку вне окна. Однако вследствие работы алгоритма отсечения, реализованного внутри GDI, видна будет только часть прямой, лежащая внутри клиентской области. void DlgOnMouseMove(HWND hDlg, int x, int y, UINT keyFlags) { if ( IsDragging ) { CurrentX = x ; CurrentY = у ; InvalidateRect(hDlg,NULL,FALSE) ; UpdateWindow (hDlg) ,- DragStop.x = x
Рисование объектов 1НПМШ 125 Dragstop.у = у ; 1 ) Сообщения о перемещении мыши начинают поступать в диалоговую проце- дуру приложения, когда мышь входит в его окно. Поскольку это может случиться даже тогда, когда пользователь не рисует контур, то в первой строке показанной выше функции проверяется, поднят ли флаг IsDragging. Иными словами, про- грамма смотрит, находится ли она в состоянии рисования контура или нет. Убедившись, что надо рисовать контур, обработчик обновляет координаты (CurrentX, CurrentY), соответствующие конечной точке нового отрезка. Значе- ния берутся из переданных обработчику аргументов (х, у). Далее вызываются функции InvalidateRect и UpdateWindow из Win32 API. Это обеспечивает немедленную перерисовку окна. СОВЕТ Если программа не станет сразу же перерисовывать окно, то будут наблюдаться значительные задержки в обновлении клиентской области. В результате пользо- ватель будет одновременно видеть старый и новый отрезки. Последним аргументом функции InvalidateRect передается FALSE. Тем са- мым подавляется перерисовка фона клиентской области, поэтому время тратится только на перерисовку изображения в области, явно указанной в сообщении WM_PAINT. Такая оптимизация уменьшает нагрузку на ЦП и устраняет мигание во время обновления окна. После того как старый отрезок стерт, а новый нарисован, текущая точка (х, у) становится концом старого отрезка и сохраняется в статической переменной DragStop. void DlgOnLButtonUp(HWND hDlg, int x, int y, UINT keyFlags) { if ( IsDragging ) { Releasecapture() ; IsDragging = FALSE ; 1 1 Проведя линию, пользователь отпускает левую кнопку мыши. В этот момент оконной процедуре приходит сообщение WM_LBUTTONUP, которое передается обработчику DlgOnLButtonUp. Проверив, что программа находится в состоянии рисования, этот обработ- чик выполняет всего два действия. Сначала вызывается функция Release- Capture, чтобы известить Windows о необходимости вернуться к стандартному алгоритму доставки сообщений мыши, то есть помещению их в очередь того окна, в котором в данный момент находится курсор. Затем флаг IsDragging сбра- сывается, в результате чего весь код рисования эластичного контура перестает выполняться.
126 ЗНШ11И1 Реализация программы рисования Модификация обработчика WMJPAINT для поддержки стирания и рисования Обработчик сообщений о перемещении мыши заставляет Windows выполнить обработку сообщения WM_PAINT, что приводит к вызову функции DlgOnPaint. Ее задача - стереть старый отрезок и нарисовать новый void DlgOnPaint(HWND hDlg) { // Строки, рассмотренные выше, опущены // Стереть старый отрезок SetROP2(DeviceContext,R2_NOTXORPEN) ; DrawLineShapeAt(DeviceContext, DragStart.x,DragStart.y, DragStop.x,DragStop.у, 2, PS_SOLID, RGB(255,0,0) ) ; // Нарисовать новый отрезок SetROP2(DeviceContext,R2_COPYPEN) ; DrawLineShapeAt(DeviceContext, DragStart.x,DragStart.y, CurrentX, CurrentY, 2, PS_SOLID, RGB (255,0, 0) ) ; } Для эффективного стирания и рисования применяется так называемая бинар- ная растровая операция, поддерживаемая GDI. Она описывает, как именно надо комбинировать значения пикселей. В Windows каждый пиксель - это сочетание красного, зеленого и синего цветов, представленное 32-разрядным числом. Рас- тровая операция определяет, как пиксели пера или кисти объединяются с пиксе- лями клиентской области. Примером растровой операции служит R2_COPYPEN. Она просто копирует пиксели пера в клиентскую область, заменяя ранее находившиеся там. Еще одна полезная операция называется R2_NOTXORPEN. В этом случае новый пиксель вычисляется как инверсия результата применения бинарной операции XOR к пикселям пера и клиентской области. На псевдокоде эту операцию можно опи- сать так: if (пиксель клиентской области *= фоновому пикселю) заменить пиксель клиентской области на фоновый пиксель ; else скопировать пиксель пера в пиксель клиентской области ; ПРИМЕЧАНИЕ Достоинство операции R2_N0TX0RPEN в том, что она позволяет быстро стереть изображение в клиентской области. Пиксели рисуемой линии заменяются фоно- выми пикселями, а это и означает стирание. Таким образом, мы можем сосре- доточиться только на пикселях, расположенных вдоль стираемой линии, а не сти- рать все содержимое охватывающего прямоугольника или клиентской области в целом.
127 Ввод и эхо-вывод символов IIIIIHHIMI Итак, для рисования линии мы применяем растровую операцию R2_COPYPEN, которая копирует пиксели пера или кисти в клиентскую область, а для стирания ранее нарисованной линии - операцию R2_NOTXORPEN. В итоге логика обработчика сообщения WM_PAINT выглядит так. 1. Вызвать функцию SetROP2 для установки в контекст устройства растро- вой операции R2_NOTXORPEN. 2. Нарисовать старый отрезок, соединяющий точки DragStart и DragStop. При этом на самом деле отрезок стирается. 3. С помощью функции SetROP2 установить в контекст устройства растро- вую операцию R2_COPYPEN. 4. Нарисовать новый отрезок, соединяющий точки DragStart и (CurrentX, CurrentY). Для рисования вызывается инкапсулированная функция DrawLineShapeAt, рассмотренная в главе 4. Во время стирания и рисования модифицируются только пиксели, лежащие на соответствующем отрезке, а значения остальных не изменяются. В результате мы добиваемся максимальной производительности и сводим к минимуму время обновления экрана. Ввод и эхо-вывод символов Следующий шаг - научить программу рисовать текстовые метки. Это полезно, если пользователь захочет подписать рисунок или поименовать оси координат. Мы сосредоточимся на самом механизме эхо-вывода символов, не отвлекаясь на другие особенности приложения. СОВЕТ Принципы эхо-вывода ничем не отличаются от применяемых в других аналогич- ных ситуациях, например в программах эмуляции терминала и в текстовых про- цессорах. На рис. 5.5 представлен графический интерфейс, описывающий возможности ввода и вывода, которые мы собираемся добавить в программу рисования. Пользователь переводит программу в режим ввода текста. В этом режиме ото- бражается каре. Когда пользователь печатает на клавиатуре, вводимые символы отображаются в окне приложения. При этом каре сдвигается вправо. Наличие каре говорит пользователю, что приложение находится в режиме ввода текста. Кроме того, оно показывает, где появится следующий символ. Закончив ввод, пользователь выходит из режима ввода текста, и программа убирает каре с экрана. Пронумерованные надписи на рис. 5.5 иллюстрируют действия пользователя. Вот как выглядит их последовательность. 1. Пользователь входит в режим ввода текста, нажимая и удерживая клавишу BACKSPACE и одновременно щелкая мышью в том месте, где должен рас- полагаться текст. В этом месте появляется каре.
128 Реализация программы рисования Character Processing Program Quit 1. Нажать клавишу BACKSPACE и отпустить левую кнопку мыши 4. Курсор Text String I* 2. Ввести символы 5. Каре 3. Нажать клавишу BACKSPACE Рис. 5.5. Пользовательский интерфейс для обработки ввода символов 2. При вводе очередного символа каре исчезает, новый символ добавляется в конец строки, после чего каре снова появляется. 3. Для выхода из режима ввода текста надо снова нажать клавишу BACKSPACE. Каре пропадает. ПРИМЕЧАНИЕ Перед началом ввода текста нужно выполнить два действия. Нажатие клавиши BACKSPACE говорит о входе в режим ввода, а щелчок левой кнопкой мыши опре- деляет положение текста. Эти действия должны выполняться строго в указанной последовательности. Ясно, что пользователю это неудобно. На настольном ПК можно было бы просто щелкнуть правой кнопкой мыши. Повторный щелчок озна- чал бы выход из режима ввода текста. Увы, в случае Pocket PC правой кнопки мыши нет, так что описанная процедура адаптирует программу к особенностям аппаратуры. На рис. 5.5 курсор мыши и каре различаются, и это не случайно. Курсор - гра- фический объект, показывающий текущее положение мыши. Windows автомати- чески отображает и перемещает его по мере того, как пользователь двигает мышь по экрану. Каре же управляется исключительно прикладной программой. Это графический объект, используемый во время операций ввода текста. Он показы- вает положение очередного символа. Программа должна проешь Windows пот
Ввод и эхо-вывод символов !!! 129 зать каре, скрыть каре и переместить его при возникновении определенных усло- вий. В этом разделе мы покажем, как работать с каре. Поскольку за перемещение каре отвечает приложение, необходимо разрабо- тать алгоритм и данные для него. Нам прежде всего необходимо знать положение каре после ввода очередного символа. На рис. 5.6 показано, на основе каких дан- ных оно вычисляется. Когда пользователь входит в режим ввода текста, программа сохраняет поло- жение курсора в статической переменной TextLocation типа POINT, то есть коор- динаты начала строки равны (TextLocation.x, TextLocation.y). По мере ввода символов программа вычисляет смещение каре от начальной точки. Для этого нужно знать ширину отображаемой строки текста, включая и только что введенный символ. С каждой строкой ассоциирован охватывающий прямоугольник, размеры которого зависят от характеристик используемого шрифта. Рис. 5.6. Вычисление положения каре на основе охватывающего прямоугольника Для вычисления смещения каре вдоль оси X программа просто прибавляет к аб- сциссе начальной точки ширину охватывающего прямоугольника. Координата Y не изменяется. Таким образом, алгоритм описывается следующими уравнениями: CaretXLocation = TextLocation.x + BoundingRectangleWidth ; CaretYLocation == Те--- *• -ocation. у ;
130 Реализация программы рисования СОВЕТ ________________________________________________________________ Для вывода текстовой строки применяется функция ExtTextOut из Win32 API. Она предполагает, что точка с координатами (TextLocation.x, TextLocation.y) находится в левом верхнем углу охватывающего прямоугольника. Для реализации эхо-вывода символов нужно прежде всего понять механизм обработки нажатий клавиш. Он схематически представлен на рис. 5.7. 1. После входа в режим ввода текста пользователь нажимает клавишу. 2. В ответ Windows генерирует сообщение WM_CHAR и доставляет его диа- логовой процедуре. Вызывается обработчик сообщения, который получает код символа, нарисованного на нажатой клавише. 3. Обработчик помещает символ в конец буфера TextData. 4. Обновив переменную TextData, обработчик вызывает функции Invalidate- Rect и Update Window, чтобы перерисовать содержимое клиентской области. 5. Windows генерирует сообщение WM_PAINT и сразу же вызывает его обра- ботчик. 6. Обработчик сообщения WM_PAINT перерисовывает текстовую строку, хранящуюся в буфере TextData. 7. В клиентской области окна приложения появляется измененная текстовая строка, в конец которой добавлен новый символ. 1. Пользователь 7. Содержимое буфера вводит символ отображается в окне 2.(WM CHARl 4 JnyalidateRect, UpdateWindow __5JWM PAINTJ 3. Сохраненные данные TextData Символ добавляется в буфер 6.Рисование с использованием сохраненных данных Рис. 5.7. Обработка сообщений о нажатии символа Эта последовательность выполняется при каждом нажатии клавиши, соот- ветствующей печатаемому символу. Последний введенный символ оказывается в конце строки, то есть программа реализует эхо-вывод. СОВЕТ Это еще один пример событийно-ориентированной модели рисования, изобра- женной на рис. 1.10. Здесь, как и в программе рисования эластичного контура, мы отделяем управление данными от обновления клиентской области. Но из приведенного выше описания не видно, как происходит работа с каре. На рис. 5.8 это упущение исправлено.
Ввод и эхо-вывод символов 1IIIII 131 Сравнив рис. 5.7 и 5.8, мы обнаруживаем дополнительные шаги, которые не- обходимо предпринять после ввода и вывода символа, чтобы переместить каре. 1. В обработчике сообщения WM_CHAR вычислить новое положение каре. С помощью функции SendMessage послать Windows сообщение WM_POSITIONCARET. 1. Пользователь 7. Содержимое буфера вводит символ отображается в окне 2.(WM CHAR£ 4. InvalidateRect, UpdateWindow 5jWM PAINT 3. Сохраненные данные TextData Символ добавляется в буфер 11. Каре переместилось 6. Рисование с использованием сохраненных данных 8. SendMessage 9^WM POSITIONCAREf] CaretLocation 10. Переместить каре в точку CaretLocation Рис. 5.8. Обновление каре с помощью отправки сообщения 2. Координаты нового положения каре передаются в параметрах сообщения WM_POSITIONCARET. 3. Обработчик сообщения WM_POSITIONCARET перемещает каре в новое положение. 4. Программа прячет каре, перемещает его в позицию за последним введен- ным символом и снова показывает. В результате пристального изучения документации по Win32 API выясняет- ся, что сообщения WM_POSITIONCARET не существует. На самом деле это со- общение, специфичное для прикладной программы. Приложение само должно определить код и объявить анализатор такого сообщения. В его обработчике для перемещения каре в новое положение вызываются функции HideCaret, Set- CaretPos и ShowCaret. СОВЕТ Очевидно, что для перемещения каре нестандартное сообщение не является не- обходимым. То же самое можно было бы сделать с помощью вспомогательной функции. Но часто в программе удобно использовать одно или несколько нестан- дартных сообщений. Поэтому мы решили показать, как можно реализовать их обработку.
132 Illi Реализация программы рисования Ha рис. 5.7 и 5.8 видно, что обработка ввода и вывода символов начинается с получения сообщения WM_CHAR. Это сообщение никогда не попадает в оче- редь основного потока. Windows генерирует его после того, как в цикле выбрано и передано функции TranslateMessage сообщение WM_KEYDOWN. Поскольку в рассматриваемой программе цикл выборки сообщений находится внутри функ- ции DialogBox, то вставить в него обращение к TranslateMessage невозможно. По- этому обработчик сообщения WM_KEYDOWN напрямую вызывает обработчик WM_CH AR. Ниже представлена логика обработчика WM_KEYDOWN: преобразовать код клавиши в код символа ; if это алфавитно-цифровая клавиша then if нажата клавиша Shift then преобразовать символ из верхнего регистра в нижний end if передать символ обработчику сообщения WM_CHAR end if Хотя такой подход к имитации сообщений WM_CHAR кажется искусствен- ным, но его достоинства намного перевешивают отсутствие истинных сообщений WMCHAR. ПРИМЕЧАНИЕ Применение функции DialogBox открывает доступ к редактору диалогов для констру- ирования сложных интерфейсов путем перетаскивания. Эго существенно повышает продуктивность и ускоряет выход на рынок. Платил, за это приходится симуляцией обработки символов. Поскольку строить графические интерфейсы для КПК приходит- ся куда чаще, чем поддерживать ввод и вывод символов, то результат стоит неболь- шой жертвы в виде обходного способа обработки сообщений WMCHAR. В этой главе мы рассмотрим еще один пример инкапсуляции - функции обра- ботки текста. Так, мы будем многократно вычислять размеры прямоугольника, охватывающего текстовую строку. В файле TextFns.c среди прочих находится и функция для решения этой задачи. Вот ее прототип: void GetTextRectangle(HWND hWindow, TCHAR * IpszInputString, int iStringSize, int iLocationX, int iLocationY, LPRECT IprctRectangleArea) ; Для поддержки ввода / вывода символов нужно внести в программу следую- щие изменения. 1. Инкапсулировать функции для работы с текстом, реализующие наиболее часто применяемые средства. 2. Добавить статические переменные, необходимые для отслеживания входа в режим ввода текста и хранения текстовой строки. 3. Определить код и анализатор нестандартного сообщения WM_POSITION- CARET. 4. Включить в программу обработчики сообщений WM_KEYDOWN, WM_CH AR и WM_POSITIONCARET.
133 Ввод и эхо-вывод символов llllinM 5. В обработчике сообщения WM_KEYDOWN инициировать вход в режим ввода текста, обработки символов и выход из этого режима. 6. В обработчик сообщения WM_LBUTTONDOWN добавить запоминание начального положения строки и создание каре. 7. В обработчике сообщения WM_CHAR выполнять операции по обработке текста и манипулированию каре, показанные на рис. 5.8. 8. В обработчике сообщения WM_POSITIONCARET перемещать каре в ко- нец отображаемой строки. 9. В обработчике сообщения WM_PAINT отображать строку, находящуюся в буфере. В следующих разделах показан и проанализирован код, реализующий эти из- менения. Реализация функций, инкапсулирующих работу с текстом Поскольку эта программа, как и многие другие из настоящей книги, работает с текстом, то мы включили в файл TextFns.c ряд часто используемых в подобных задачах функций. Ниже мы обсудим лишь те из них, что применяются в данном приложении. Остальные будут рассмотрены, когда возникнет необходимость. Часто обработчик сообщения должен вычислять размеры прямоугольника, охватывающего текстовую строку. Функция GetRectangle делает это, обращаясь к нескольким функциям Win32 APL void GetTextRectangle(HWND hWindow, TCHAR * IpszInputString, int iStringSize, int iLocationX, int iLocationY, LPRECT LprctRectangleArea) ( SIZE Textsize ; HDC hDC ; hDC = GetDC(hWindow) ; GetTextExtentPoint32 ( hDC, IpszInputString, iStringSize, STextSize ) ; ReleaseDC(hWindow,hDC) ; lprctRectangleArea->top = iLocationY ; lprctRectangleArea->left = iLocationX ; LprctRectangleArea->right = iLocationX + Textsize.ex ; lprctRectangleArea->bottom = iLocationY + Textsize.cy ; } Первым аргументом ей передается описатель hWindow окна, в которое выво- дится текст. Далее следуют сама строка IpszInputString и число символов в ней iStringSize. Поскольку эту программу предстоит переносить на Pocket PC, то для представления строки выбран переносимый тип TCHAR. Для вычисления охва- тывающего прямоугольника функции необходимо знать его левый верхний угол, который определен с помощью аргументов iLocationX и iLocationY. Результирую- щий прямоугольник возвращается вызывающей программе в структуре RECT, на которую указывает аргумент IprctRectangleArea типа LPRECT (длинный указа-
134 'Illi Реализация программы рисования тель на RECT). По определению (см. файл windows.h) структура RECT состоит из четырех полей: top, left, right и bottom. яиу] СОВЕТ fjl X, j it х.. :<- - i-.Tir ii4>iwi!u¥ ~Hiiiihiiiiiiii |||П1|Гги<|1> nnrniirrrr‘ппгг~"гпт1Щ[| itr~rnrr<i'rr~‘~~TiririnHrurrTtrrrr rr‘rrirr iriir'niiii uir n’lrmrmmtiinTtiiiMirrnrirr-irrrinrrirrmT'riiii^niTnr'rn ifiw*ji‘Mn‘niriinirrr",innni‘i'n‘n‘ri‘nx‘nririnT i f i.1 t—r'n.T-rrrf’ni—-1'— «к \ \ Термин «длинный указатель» (long pointer) восходит еще к 16-разрядной версии qVpJ Windows 3.1. Чтобы сослаться на ячейку памяти, отстоящую дальше чем на 64 Кб, L-------------- нужно было явно объявлять длинный указатель. В первоначальном варианте Windows API компания Microsoft любезно определила множество типов, в частно- сти LPRECT, являющихся длинными указателями. С появлением 32-разрядной Windows 95 необходимость в таком объявлении отпала. Но поскольку во всех про- граммах для Windows эти вспомогательные типы используются, то Win32 API про- должает их поддерживать. Так как «длина» указателя уже не важна, то в заголо- вочных файлах Win32 API LPRECT определен просто как RECT *, то есть указатель на структуру RECT. Для вычисления охватывающего прямоугольника эта функция сначала опре- деляет длину и высоту текстовой строки. Затем эти величины прибавляются к левому верхнему углу прямоугольника для получения его правого нижнего угла. Для вычисления размеров строки необходим контекст устройства. Прило- жение может запросить его в любое время (не только внутри обработчика WM_PAINT) с помощью функции GetDC, которая возвращает контекст устрой- ства (hDC), зная описатель окна (hWindow). В контексте устройства хранится, в частности, шрифт. Зная шрифт, можно вычислить размеры текстовой строки, для чего в Win32 API предназначена функ- ция GetTextExtentPoint32. Ее первым аргументом является описатель контекста (hDC), затем передаются адрес начала строки (IpszInputString) и число символов в ней (iStringSize). Результат Windows возвращает в переменной TextSize типа SIZE. Согласно определению в Win32 API, эта структура состоит из двух полей: сх и су. Ширина текстовой строки хранится в поле TextSize.cx, а высота - в поле TextSize.cy. Вычислив размеры строки, функция возвращает контекст устройства Windows, вызывая ReleaseDC. Возвращать контекст необходимо, чтобы им могли воспользоваться другие приложения. Число контекстов устройств в Windows ограничено, поэтому пренебрежение этой операцией может привести к неустой- чивой работе системы. На последнем шаге наша вспомогательная функция копирует размеры охва- тывающего прямоугольника в переданную структуру RECT. Значения left и top совпадают с переданными аргументами iLocationX и iLocationY соответственно. Для заполнения полей right и bottom нужно прибавить к координатам левого верхнего угла размеры строки. Так, величина bottom равна iLocationY плюс высо- та строки, хранящаяся в TextSize.cy. Для реализации других функций в TextFns можно воспользоваться плодами ' уже проделанной работы. Именно так мы и поступили с функцией GetTextWidth,
Ввод и эхо-вывод символов 1НИП 135 которая сначала вычисляет охватывающий прямоугольник, а потом использует результат для определения ширины текста: void GetTextWidth( HWND hWindow, TCHAR * IpszInputString, int iStringSize, int * iTextWidth ) { RECT rctTmpRect ; GetTextRectangle(hWindow,IpszInputString,iStringSize,0,0,SrctTmpRect) ; ‘iTextWidth = rctTmpRect.right ; } Вычислив охватывающий прямоугольник с помощью GetTextRectangle, эта функция берет ширину текстовой строки из временной структуры rctTmpRect. Ясно, что ширина строки равна разности между ординатами правой и левой сто- рон прямоугольника. Воспользовавшись существующей функцией, программист избегает необходимости повторно вводить и отлаживать уже работающий код. Добавление переменных для хранения состояния и текстовой строки Переменные, необходимые для поддержки ввода текста, можно отнести к двум категориям. Переменные состояния отслеживают действия, которые пользователь выполняет, чтобы войти в режим ввода текста и покинуть его, а чис- ловые и строковые переменные хранят данные, относящиеся собственно к обра- ботке текста. static BOOL TypingText = FALSE ; static BOOL Textlnitialized = FALSE ; static BOOL PositionSet = FALSE ; static POINT TextLocation ; static TCHAR TextData[50] ; static int CurrentChar = 0 ; Здесь первые три переменные нужны для отслеживания информации о теку- щем состоянии ввода текста. Переменная TypingText определяет, находится ли пользователь в режиме ввода текста. Она устанавливается при входе в этот режим и сбрасывается при выходе из него. Для управления отображением текстовой строки служит переменная Textlnitialized. Когда пользователь входит в режим ввода текста, она устанавливается в TRUE и сохраняет это значение даже после завершения ввода, чтобы обработчик WM_PAINT продолжал отображать текст при перерисовке клиентской области. Поскольку пользователь сначала входит в режим ввода, а потом щелкает мышью, чтобы обозначить начальную точку, то необходима переменная PositionSet, которая говорит о том, что начальная точка определена. Для хранения координат правого верхнего угла охватывающего прямоуголь- ника служит переменная TextLocation. Это структура типа POINT, в которой есть два поля: х и у. Сама строка хранится в буфере, на начало которого указывает пе- ременная TextData Так как программа будет переноситься на Pocket PC, буфер
136 Illi Реализация программы рисования состоит из символов типа TCHAR. Номер следующей свободной позиции в буфе- ре TextData хранится в переменной CurrentChar. Обработчик сообщения WM_POSITIONCARET Любая программа для Windows может определять нестандартные сообщения. Для этого нужно задать код сообщения и сгенерировать его анализатор. Той дру- гое показано в следующем фрагменте: «define WM_POSITIONCARET WM_USER + 0x100 «define HANDLE_DLG_WM_POSITIONCARET(hDlg, wParam, IParam, fn) \ ( (fn)((hDlg),(int)(wParam),(int)(IParam) ), OL) В определении кода нового сообщения фигурирует константа WM_USER, являющаяся частью Windows API. Код символа WM_POSITIONCARET образу- ется путем прибавления смещения (шестнадцатиричное 100 в данном случае) к минимальному номеру, зарезерированному для нестандартных сообщений. СОВЕТ ж», \\ Первоначально предполагалось, что, начиная с константы WMJUSER, должны ау%=1 идти коды нестандартных сообщений. Microsoft обещала, что не будет заводить ----------- системных сообщений с кодами, большими этого значения. Но кто-то в Microsoft невнимательно прочел документацию и присвоил сообщениям от системных эле- ментов управления (см. главу 8) коды от WMJJSER до WMUSER+100. Поэтому теперь коды нестандартных сообщений должны начинаться с WMJJSER+100! Для реализации анализатора нового сообщения придется немного поработать. Анализатор вставляется в предложение switch в диалоговой процедуре следую- щим образом: HANDLE_DLG_MSG(hDlg, WM_POSITIONCARET, DlgOnPositionCaret) ; Этот макрос уже определен в заголовочном файле windowsy.h: «define HANDLE_DLG_MSG(hwnd, message, fn) \ case (message): return HANDLE_DLG_##message((hwnd), (wParam), \ (IParam), (fn)) При расширении переданный в аргументе message код сообщения подставля- ется в строку HANDLE_DLG_##message. Чтобы реализовать анализатор сообще- ния WM_POSITIONCARET, программа должна объявить макрос с именем HANDLE_DLG_WM_POSITIONCARET. Он извещает Windows, как отобразить параметры wParam и IParam на аргументы обработчика. В данном случае этот макрос следует определить так: «define HANDLE_DLG_WM_POSITIONCARET(hDlg, wParam, IParam, fn) \ ( (fn)((hDlg),(int)(wParam),(int)(IParam) ), OL) Как видим, обработчик сообщения принимает два аргумента, потому что спи- сок подставляемых аргументов в макросе содержит именно столько элементов. Анализатор преобразует значение wParam к типу int и передает его обработчику в качестве первого аргумента. Параметр IParam также преобразуется к т,'пу int и
Ввод и эхо-вывод символов ИИИП 137 передается обработчику. В конце макроса стоит 0L. Это число вызывающая про- грамма получает в качестве возвращаемого значения при обращении к HANDLE_ DLG MSG из предложения switch. Пока осталось неясным назначение аргументов обработчика сообщения. На самом деле они могут представлять все, что угодно, лишь бы отправитель и полу- чатель обрабатывали их согласованно. Конкретный смысл становится понятен, если взглянуть на прототип обработчика сообщения: void DlgOnPositionCaret(HWND hDlg, int CaretXLocation, int CaretYLocation) ; Как видим, wParam и IParam определяют новое положение каре. ПРИМЕЧАНИЕ Описанная выше схема анализа этого сообщения будет работать, только если отправитель помещает координаты требуемого положения каре в параметры wParam и IParam. Добавление обработки сообщений о введенных символах Без этих сообщений обработать ввод и вывод символов было бы невозможно. Все подготовительные операции выполняются в обработчике DlgOnKeyDown сообщения WM_KEYDOWN, а обработка одного символа - в обработчике DlgOnChar сообщения WM_CHAR. Управлением каре занимается обработчик DlgOnPositionCaret. void DlgOnChar(HWND hDlg, UINT ch, int cRepeat); void DlgOnKeyDown(HWND hDlg, UINT vk, BOOL fDown, int cRepeat, UINT flags) void DlgOnPositionCaret(HWND hDlg, int CaretXLocation, int CaretYLocation) BOOL CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) ( switch (message) { // Строки, рассмотренные выше, опущены HANDLE_DLG_MSG(hDlg, WM_CHAR, DlgOnChar); HANDLE_DLG_MSG(hDlg, WM_KEYDOWN, DlgOnKeyDown); HANDLE_DLG_MSG(hDlg, WM_POSITIONCARET, DlgOnPositionCaret) ; ) return FALSE ; ) Как и все прочие обработчики сообщений, эти тоже получают в качестве пер- вого аргумента описатель окна hDlg типа HWND. Остальные аргументы отража- ют специфику конкретного сообщения. void DlgOnKeyDown(HWND hDlg, UINT vk, BOOL fDown, int cRepeat, UINT flags) ;
138 Illi Реализация программы рисования Вторым аргументом обработчику передается код виртуальной клавиши vk. Он описывает нажатую клавишу способом, не зависящим от производителя конк- ретной клавиатуры. В результате замена одной клавиатуры на другую не потребу- ет никаких изменений в коде программы. Следующие два аргумента относятся к случаю, когда пользователь нажимает одну клавишу несколько раз подряд. Аргумент fDown показывает, была ли клави- ша нажата один или несколько раз. Если он равен TRUE, то аргумент cRepeat го- ворит, сколько раз была нажата клавиша, прежде чем программа получила сооб- щение. Аргумент flags описывает ситуации, которые не относятся к Pocket PC, по- скольку в этом случае клавиатура реализована на экране. Следовательно, в про- граммах, предназначенных для Pocket PC, этот аргумент можно смело игнориро- вать. void DlgOnChar(HWND hDlg, UINT ch, int cRepeat); Наибольший интерес представляет аргумент ch. Он содержит ASCII-код об- рабатываемого символа. В программах для Windows тип UINT взаимозаменяем с char, WCHAR и TCHAR. СОВЕТ ASCII-код символа и код виртуальной клавиши - совершенно разные вещи. Коди- ровка ASCII определена независимой организацией - Национальным институтом стандартизации США (ANSI). Коды виртуальных клавиш определены компанией Microsoft, а трансляцию выполняет драйвер клавиатуры. Поскольку в сообщении WM_KEYDOWN находится код виртуальной клавиши, то обработчик этого сооб- щения должен преобразовать его в ASCII-код и только потом вызывать DlgOnChar. Реализация обработчика сообщения WM_KEYDOWN Функция DlgOn Key Down выполняет большую часть работы по вводу и эхо- выводу символов. Она отслеживает вход в режим ввода текста и выход из него, а также все операции, связанные с клавиатурой. void DlgOnKeyDown(HWND hDlg, UINT vk, BOOL fDown, int cRepeat, UINT flags) ( TCHAR Character ; SHORT ShiftPressed ; switch( vk ) { case VK_BACK: if ( 'TypingText ) { TypingText = TRUE ; Textlnitialized = TRUE ; _tcscpy(TextData,_TEXT("") ); CurrentChar = 0 ,
139 Ввод и эхо-вывод символов } else { TypingText = FALSE ; PositionSet = FALSE ; HideCaret(hDlg) ; DestroyCaret() ; } break ; default: if ( PositionSet ) { Character = MapVirtualKey(vk,2) ; if ( IsCharAlphaNumeric(Character) I I (Character == ' ') ) { ShiftPressed = GetKeyState(VK_SHIFT) ; if ( ShiftPressed >= 0 ) Character = _totlower(Character) ; DlgOnChar(hDlg,Character, cRepeat) ; } } break ; } } Прежде всего обработчик должен решить, что надо делать. Алгоритм приня- тия решения можно описать на псевдокоде следующим образом: if ( нажата backspace и не в режиме ввода текста ) then войти в режим ввода текста ; else if (нажата backspace и в режиме ввода текста ) then выйти из режима ввода текста ; else выполнять обработку вводимых символов и эхо-вывод ; end if Для реализации этого алгоритма потребуется предложение switch с вложен- ными if и ветвью default для обработки последнего случая. Чтобы понять, была ли нажата клавиша BACKSPACE, обработчик сравнивает код виртуальной клавиши в аргументе vk с константой VK_BACK. Вход и выход из режима ввода текста реализовать легко..Нужно лишь присво- ить значения некоторым переменным, описывающим состояние и данные. Важно при этом не забыть очистить буфер TextData. СОВЕТ HW ivi.il Если не инициализировать буфер TextData, то обработчик WM PAINT отобразит находящийся в нем «мусор». Вряд ли такое поведение понравится пользователю. По завершении ввода символов обработчик должен также скрыть и уничто- жить каре. Для этого в Win32 API есть функции с говорящими названиями HideCaret и DestroyCaret. Вызов DestroyCaret особенно важен. Windows поддер- живает всего одно каре для всех приложений Если программа забудет вызвать
140 {III! Реализация программы рисования DestroyCaret, то каре не будет возвращено системе. Попытка получить каре в лю- бой другой программе (вскоре мы рассмотрим эту операцию) закончится неуда- чей, и приложение будет вести себя странно. Более интересна последовательность действий, выполняемых, когда пользо- ватель уже находится в режиме ввода текста. Код виртуальной клавиши vk преоб- разуется в ASCII-код символа путем обращения к функции MapVirtualKey из Win32 API. Эта функция всегда возвращает символы верхнего регистра. Если на- жата клавиша, соответствующая букве, цифре или пробелу, обработчик выводит это на экран. Получив символ в верхнем регистре, обработчик проверяет состояние клави- ши SHIFT, обращаясь к функции GetKeyState, и при необходимости переводит символ в нижний регистр с помощью _totlower. Мы применяем здесь переноси- мый вариант функции преобразования, так как программа будет переноситься на Pocket PC. И в конце код символа и счетчик повторений cRepeat передаются обра- ботчику DlgOnChar сообщения WM_CHAR. ПРИМЕЧАНИЕ Этот алгоритм повторяет действия, которые выполнила бы функция TranslateMessage, будь она вызвана в цикле выборки сообщений. Но поскольку этот цикл скрыт в функции DialogBox и вставить обращение к TranslateMessage мы не можем, то приходится выполнять обработку самостоятельно. Модификация обработчика сообщений WMLBUTTONDOWN В рассматриваемой программе левая кнопка мыши используется для двух це- лей. Если пользователь еще не вошел в режим ввода текста, то щелчок левой кноп- кой означает начало рисования эластичного контура. Если же перед этим была нажата клавиша BACKSPACE, то щелчок фиксирует положение левого верхнего угла прямоугольника, охватывающего текстовую строку. Псевдокод, описывающий двойственность левой кнопки мыши, выглядит так: if ( не в режиме ввода текста ) then начать рисование эластичного контура else отметить начало текста end if Эта логика реализована в обработчике DlgOnLButtonDown сообщения WM_LBUTTONDOWN. Переменная TypingText управляет выбором нужной ветви. void DlgOnLButtonDown(HWND hDlg, BOOL fDoubleClick, int x, int y, UINT keyFlags) ( int TextHeight
Ввод и эхо-вывод символов 141 if ( !TypingText ) { IsDragging = TRUE ; SetCapture(hDlg) ; DragStart.x = x ; DragStart.у = у ; DragStop.x = x ; DragStop.у = у ; } else { if (!PositionSet) { PositionSet = TRUE ; TextLocation.x = x ; TextLocation.у = у ; GetTextHeight(hDlg,__TEXT("W”),1,«TextHeight) ; CreateCaret(hDlg,NULL,2,TextHeight) ; SetCaretPos(TextLocation.x,TextLocation.y) ; ShowCaret(hDlg) ; } } } Код, относящийся к рисованию контура, находится в ветви if, мы его уже об- суждали выше. Никаких изменений, кроме помещения в ветвь if, не требуется. В ветви else обработчик готовит данные для ввода текста. Убедившись, что начальная позиция еще не инициализирована, обработчик выполняет инициали- зацию и захватывает ресурсы. В переменную PositionSet записывается значение TRUE, это означает, что начальная позиция установлена. Затем в переменную TextLocation заносятся текущие координаты курсора мыши. Единственный ре- сурс, который надо захватить для ввода текста, - это каре. Прежде чем захватить каре, обработчик сообщения вызывает функцию GetTextHeight из компонента TextFns, чтобы определить необходимую высоту каре. Для этой цели лучше всего подходит символ W, поскольку он обычно самый высокий в шрифте. Так как программа будет переноситься на Pocket PC, строка из одного символа W погружается в макрос_____TEXT. В Windows есть только одно каре для всех приложений. Если приложение хо- чет показать каре, оно должно захватить этот ресурс с помощью функции CreateCaret, которой передаются ширина каре (2) и его высота (TextHeight). Получив в свое распоряжение каре, обработчик устанавливает его положение, обращаясь к функции SetCaretPos, которой передает координаты TextLocation.x и TextLocation.y. Последний шаг - вызов функции ShowCaret, чтобы Windows ото- бразила каре в указанной позиции и окне. При вызове SetCaretPos передавать описателю каре не нужно. Так как Windows поддерживает только одно каре, то его описатель хранится внутри си- стемы. Тем самым доступ к каре инкапсулирован, что освобождает программиста от лишних забот.
142 Illi Реализация программы рисования Реализация обработчика сообщения WM_CHAR В режиме ввода текста обработчик сообщения WM_KEYDOWN передает введенный символ функции DlgOnChar. Та добавляет символ в конец текстового буфера, отображает строку в клиентской области и перемещает каре. void DlgOnChar(HWND hDlg, UINT ch, int cRepeat) { RECT TextRect ; int Textwidth ; if (PositionSet) { TextData[CurrentChar] = ch ; TextData[CurrentChar+1] = '\0' ; CurrentChar = CurrentChar + 1 ; GetTextRectangle(hDlg, TextData, _tcslen(TextData) , TextLocation.x, TextLocation.y, (LPRECT)STextRect ) ; InvalidateRect(hDlg,(LPRECT) STextRect , TRUE) ; UpdateWindow(hDlg) ; GetTextWidth(hDlg,TextData,_tcslen(TextData),STextWidth) ; SendMessage(hDlg, WM_POSITIONCARET, (WPARAM)TextLocation.x + TextWidth, (LPARAM)TextLocation.у ) ; ) } Этот обработчик вызывается только после того, как в результате нажатия пользователем клавиши BACKSPACE с последующим щелчком левой кнопкой мыши была установлена начальная позиция текста. При этих условиях перемен- ная PositionSet равна TRUE, поэтому выполняется основная часть тела функ- ции. В противном случае вводимые символы появлялись бы в случайных пози- циях. Проверив выполнение необходимого условия, обработчик помещает символ, переданный в аргументе ch, в позицию буфера TextData, номер которой хранится в переменной CurrentChar. После увеличения на 1 CurrentChar указывает на по- зицию, следующую за только что добавленным символом. Перед выводом текста из буфера TextData на экран обработчик вызывает функцию GetTextRectangle для вычисления охватывающего прямоугольника. Эта функция находится в файле TextFns.c. Ей передаются сама строка TextData, ее длина, вычисленная с помощью платформенно-независимой функции _tsclen, координаты левого верхнего угла прямоугольника (TextLocation.x и Text- Location.y) и указатель на структуру RECT, в которой будет возвращен результат (&TextRect). Вызов функций InvalidateRect и UpdateWindow приводит к перерисовке окна. В качестве аргумента InvalidateRect получает указатель на структуру
Ввод и эхо-вывод символов 143 TextRect и установленный в TRUE флаг. Такая комбинация приводит к перери- совке части окна внутри охватывающего прямоугольника. ПРИМЕЧАНИЕ^ Передавая охватывающий прямоугольник и флаг TRUE, обработчик просит Windows закрасить фон только внутри этого прямоугольника. Коль скоро обновляемая об- ласть ограничена, обработчик WM_PAINTсможет перерисовать ее очень быстро, избежав мигания. Для этого ему нужно только стереть старую строку и вывести новую, не обращая внимания на прочие части клиентской области. Оставшиеся строки касаются перемещения каре. Эта процедура состоит из двух шагов. С помощью вспомогательной функции GetTextWidth обработчик по- лучает величину Text Width - число пикселей от начала текста до каре. Затем он обращается к функции SendMessage, чтобы она упаковала координаты курсора в сообщение WM_POSITIONCARET и сразу же вызвала обработчик этого сооб- щения. Горизонтальная координата курсора равна сумме абсциссы начала строки и вычисленного смещения Text Width. Функция SendMessage принадлежит Win32 API. Она приостанавливает вы- полнение диалоговой процедуры и повторно входит в нее, вызывая обработчик переданного сообщения. В этом процессе очередь сообщений основного потока не участвует, поэтому время реакции очень мало. В данном случае реакция заключа- ется в перемещении каре в позицию, следующую за вновь введенным символом. Затем управление возвращается обработчику сообщения WM_CHAR. ПРИМЕЧАНИЕ В Win32 API есть и еще один способ отправки сообщения приложению - функция PostMessage. Она помещает сообщение в очередь основного потока. В результа- те может наблюдаться некоторая задержка. Но в случае перемещения каре такая задержка нежелательна, так как пользователь хочет продолжать ввод. Реализация обработчика сообщения WMPOSITIONCARET Этот обработчик перемещает каре в новое положение. Для этого нужно снача- ла скрыть каре, затем установить новую позицию и снова показать каре: void DlgOnPositionCaret(HWND hDlg, int CaretXLocation , int CaretYLocation ) { HideCaret(hDlg) ; SetCaretPos( CaretXLocation, CaretYLocation ) ; ShowCaret(hDlg) ; ) Все эти функции уже были рассмотрены выше. Их объявления находятся в файле windows.h
144 Illi Реализация программы рисования Отображение строки в обработчике сообщения WMPAINT Отображение введенной пользователем строки - это основное дополнение к обработчику WM_PAINT. Перед выводом текста этот обработчик закрашивает фон тем же цветом, что и вся клиентская область: void DlgOnPaint(HWND hDlg) { COLORREF OldTextColor ; // Строки, рассмотренные выше, опущены if (Textlnitialized) { OldTextColor = SetBkColor(DeviceContext, RGB(255,255,255) ) ; ExtTextOut(DeviceContext, TextLocation.x, TextLocation.y, 0,NULL,TextData,_tcslen(TextData),NULL) ; SetBkColor(DeviceContext, OldTextColor) ; } } Закрасив фон, эта функция выводит текст, хранящийся в буфере TextData, предварительно удостоверившись, что буфер инициализирован (на это указывает переменная Textlnitialized). Если текст еще не был инициализирован, то код его вывода пропускается. Цвет фона текста также хранится в контексте устройства. Этим цветом закра- шивается прямоугольник, охватывающий текст. Чтобы текстовые области не вы- делялись на фоне клиентской области, обработчик вызывает функцию API SetBkColor, передавая ей ту комбинацию основных цветов, которой закрашена клиентская область. Сочетание стопроцентных долей красного, зеленого и синего дает белый цвет. Заменив цвет фона, функция SetBkColor возвращает тот цвет, который хранился в контексте перед этим. Этот цвет запоминается в переменной OldTextColor. СОВЕТ Если цвет фона охватывающего прямоугольника не соответствует цвету фона клиентской области, то текст будет отображаться на выделяющемся фоне. Это, скорее всего, вызовет у пользователя замешательство, так как он примет стати- ческий текст за кнопку и попытается нажать ее - без какого-либо эффекта. Для вывода текста обработчик вызывает функцию ExtTextOut. Windows вы- водит строку, начиная с точки с координатами (TextLocation.x, TextLocation.y) и пользуясь инструментами, хранящимися в контексте устройства. Строка бе- рется из буфера TextData и состоит из _tcslen(TextData) символов. Вслед за ко- ординатами начальной точки идет пара аргументов О, NULL. Первый говорит о том, что отсечение не требуется. Если бы вместо нуля мы задали значение ETO_CLIPPED, то второй аргумент содержал бы указатель на структуру, описы- вающую отсекающий прямоугольник. Последнийаргумент, равный в пн"ом слу-
Резюме НИИ 145 чае NULL, можно было бы заменить указателем на массив чисел, задающих до- полнительные промежутки между символами (и отступ от границ охватывающе- го прямоугольника). После того как текст выведен, обработчик восстанавливает предыдущий цвет фона в контексте устройства, вызывая функцию SetBkColor с аргументом OldTextColor. Критика подхода к проектированию и реализации Для рисования эластичного контура и ввода текста в программе заведены че- тыре переменные состояния: IsDragging, TypingText, Textlnitialized и PositionSet. Благодаря такому изобилию переменных и местами довольно «корявому» коду программу удалось-таки заставить работать правильно. Но если бы мы захотели включить в нее дополнительные возможности, то на отладку ушло бы очень много времени. Гораздо лучше применить конечный автомат, описывающий взаимодействие пользователя с программой. Тогда для добавления новых возможностей надо было бы всего лишь расширить набор состояний и реализовать код обработки. Отлаживать изменения при таком подходе было бы куда проще. В следующей главе мы разработаем каркас для применения конечного авто- мата, описывающего логику работы приложения. Это пример проектирования с разбиением на уровни, при котором получаются надежные и легко расширяемые программы. Резюме В этой главе мы разработали программу, которая позволяет рисовать с исполь- зованием эластичного контура и располагать текст в любом месте клиентской области. В приведенной реализации присутствует много переменных состояния и местами запутанный код, который мы в следующей главе заменим конечным авто- матом. Из всего изложенного материала необходимо запомнить следующее. □ Бинарные растровые операции позволяют быстро рисовать и стирать ин- дивидуальные пиксели графического объекта. □ Каре указывает позицию следующего вводимого символа. □ Для использования каре необходимо дополнительное программирование. □ Для определения нестандартного сообщения нужно завести его код и реа- лизовать анализатор. □ Ввиду отсутствия в Pocket PC правой кнопки мыши для входа в режим ввода текста и фиксации начальной позиции нужны дополнительные шаги. □ Функция GetDC позволяет программе в любой момент получить контекст устройства. □ Задав охватывающий прямоугольник и установив флаг перерисовки в TRUE, обработчик сообщения WM_PAINT может быстро стереть и пере- рисовать текстовую строку.
146 III1 Реализация программы рисования Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка Программа рисования эластичного контура для настольного ПК RubberBandingProgram Программа рисования эластичного контура для Pocket PC RubberBandingProgramPPC Программа ввода текста для настольного ПК CharacterProcessingProgram Программа ввода текста для Pocket PC Инструкции по сборке и запуску CharacterProcessingProgramPPC Программа рисования эластичного контура для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект RubberBandingProgram.dsw в папке RubberBandingProgram. 3. Соберите программу. 4. Запустите программу. 5. Поместите курсор мыши в произвольную точку. 6. Нажмите и удерживайте левую кнопку мыши. 7. Не отпуская левой кнопки, буксируйте мышь по клиентской области окна. За курсором без ощутимого мигания должна следовать красная прямая ли- ния. 8. Не отпуская левой кнопки, отбуксируйте мышь за границу окна. Красная линия должна остановиться на границе клиентской области. 9. Отпустите левую кнопку мыши. Последняя нарисованная линия должна остаться на экране. 10. Выберите пункт меню Quit. 11. Окно закроется, так как приложение завершило работу. Программа рисования эластичного контура для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект RubberBandingProgramPPC.vcw в папке RubberBanding- ProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу RubberBandingProgram. 12. Коснитесь кончиком стилоса произвольной точки на экране Pocket PC.
Примеры программ в Web IIIIMH 147 13. Не отпускайте стилос. 14. Не отпуская стилоса, ведите им по клиентской области окна. За стилосом без ощутимого мигания должна следовать красная прямая линия. 15. Не отпуская стилоса, введите его за границу окна. Красная линия должна остановиться на границе клиентской области. 16. Уберите стилос. Последняя нарисованная линия должна остаться на экране. 17. Выберите пункт меню Quit. 18. Окно закроется, так как приложение завершило работу. Программа ввода текста для настольного ПК 1. Запустите Visual C++6.0. 2. Откройте проект CharacterProcessingProgram.dsw в папке CharacterPro- cessingProgram. 3. Соберите программу. 4. Запустите программу. 5. Нажмите и не отпускайте клавишу BACKSPACE. 6. Поместите курсор мыши в произвольную точку. 7. Нажмите и отпустите левую кнопку мыши. Должно появиться каре. 8. Введите какие-нибудь символы. Они должны отобразиться в клиентской области, а каре должно перемещаться по экрану. Пробелы отображаются, но любые другие символы, кроме букв и цифр, например «!», игнорируются. 9. Нажмите и отпустите клавишу BACKSPACE. 10. Выберите пункт меню Quit. 11. Окно закроется, так как приложение завершило работу. Программа ввода текста для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект CharacterProcessingProgramPPC.vcw в папке Character- ProcessingProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу RubberBandingProgram. 12. Коснитесь стилосом иконки, которая выводит на экран изображение кла- виатуры. 13. Коснитесь клавиши BACKSPACE. 14 Коснитесь экрана стилосом. Должно появиться каре.
148 III4 Реализация программы рисования 15. Введите какие-нибудь символы. Они должны отобразиться в клиентской области, а каре должно перемещаться по экрану. Пробелы отображаются, но любые другие символы, кроме букв и цифр, например «!», игнори- руются. 16. Коснитесь клавиши BACKSPACE. 17. Коснитесь иконки, которая убирает с экрана изображение клавиатуры. 18. Выберите пункт меню Quit. 19. Окно закроется, так как приложение завершило работу.
Глава 6. Обработка растровых изображений В предыдущих главах мы разрабатывали программы, в которых использовались графические средства, имеющиеся на платформе Windows СЕ. Это были средства векторной графики, то есть мы оперировали точками в двумерном пространстве рисования. Любая точка в двумерном пространстве определяет вектор. Напри- мер, в рисовании прямой участвуют векторы, задающие начальную и конечную точки. Хотя такой подход пригоден для многих видов пользовательских интер- фейсов, у него имеется практическое ограничение. Рисование сложного изобра- жения путем последовательного применения графических команд потребовало бы слишком много процессорного времени. Для сложных изображений эффективнее оказывается растровая графика. В этом случае изображение представляет собой набор пикселей, которые про- грамма просто копирует на экран. Windows СЕ отлично поддерживает эту техни- ку с помощью растровых изображений (bitmap). На самом деле растровое изобра- жение в Windows - это комбинация множества пикселей и метаданных, которые это множество описывают. В этой главе мы разработаем набор функций, инкапсу- лирующих работу с растровыми изображениями, а затем применим их в трех раз- ных программах. Первая из них - простая программа обработки изображений. Она позволит загрузить картинку из файла, вывести ее на экран, применить алгоритм обнару- жения краев, а затем сохранить модифицированное изображение в файле. Для обнаружения краев понадобится прямой доступ к пикселям. Во второй программе демонстрируется создание заставки в клиентской обла- сти. Когда программа запускается, вся клиентская область заполняется растро- вым изображением, а по прошествии некоторого времени восстанавливается ее нормальный вид. И напоследок мы применим инкапсулированные функции к программе ани- мации изображения. Она загружает картинки для переднего и заднего планов. При каждом срабатывании таймера положение картинки на переднем плане изме- няется, что воспринимается как движение. Важной составной частью инкапсуляции является работа с изображениями как с ресурсами Windows, которые хранятся в памяти, где ими можно манипули- ровать. В большинстве книг подход к программированию растровых изображе- ний радикально отличается. Изображение загружается в память как массив бай- тов. Чтобы отобразить его на экране, программа создает растровое изображение и копирует его в клиентскую область. При таком подходе копировать пиксели при- ходится перед каждым отображением
150 1111 Обработка растровых изображений ПРИМЕЧАНИЕ Гэраздо эффективнее копировать растровые изображения из файла прямо в ре- сурс Windows. Если растровое изображение представлено в виде ресурса, то в распоряжении программиста оказывается целый ряд методов для манипулирования им через описатель ресурса. При этом копирование происходит только один раз, в момент загрузки изображения из файла, а не при каждой операции манипулирования им. Для больших изображений производительность существенно повышается. Все операции с ресурсами, представляющими растровые изображения в Windows, выполняются очень быстро, даже на Pocket PC. Это объясняется эф- фективной реализацией соответствующих функций Win32 API, основными из которых являются Bit Bit и StretchBIt. Обе предназначены для копирования пик- селей из ресурса в клиентскую область. Понимая, что их будут применять для ко- пирования большого числа пикселей, Microsoft приложила немало усилий к их оптимизации. Реализация программы обработки изображений Обработка изображений применяется в самых разных сферах. В основе мно- гих алгоритмов обработки лежит идея фильтра. Фильтр представляет собой так называемое «ядро», которое программа применяет к областям изображения. Ко- эффициенты ядра позволяют получать новые пиксели из существующих таким образом, что выявляются важные особенности изображения. В настоящем разде- ле мы рассмотрим ядро обнаружения краев. Алгоритм обнаружения краев выделяет визуально распознаваемые границы объектов, составляющих изображение. После его применения пользователь увидит набор линий в местах переходов от одного объекта к другому. Например, примене- ние к фотографии, на которой запечатлены здания, даст контуры всех строений. Для реализации программы обработки изображений понадобится несколько базовых операций над изображениями. К их числу относятся загрузка в память, выделение пикселей, запись пикселей, генерируемых алгоритмом обнаружения краев, назад в изображение и запись результата на диск. Описание пользовательского интерфейса программы На рис. 6.1 представлен пользовательский интерфейс программы обработки изображений. В частности, показано, как выбирается изображение для загрузки в память. Чтобы начать процедуру загрузки, пользователь выбирает пункт меню Select. При этом появляется ниспадающее меню. В нем пользователь выбирает пункт Input, и в результате диалоговая процедура получает сообщение WM COMMAND
Реализация программы iiiian 151 1. Щелкнуть по пункту меню Select Image Processing Program Quit ► Select Image -------► Input Output 2. Щелкнуть по пункту подменю Input Рис. 6.1. Выбор изображения для загрузки в память В обработчике сообщения WM_COMMAND программа заполняет и открыва- ет диалоговое окно File Open. Это один из нескольких стандартных диалогов, вхо- дящих в состав Win32 API. На рис. 6.2 показано, как он выглядит. Большинство пользователей знакомы с этим диалоговым окном. Оно приме- няется во многих программах, почему Microsoft и решила стандартизовать его. В верхней части окна находится панель навигации. Пользуясь иконкой Up Folder (Переход на один уровень вверх) и раскрывающимся списком Look In (Папка), пользователь сначала доходит до папки, в которой хранится файл с изображе- нием. Обработчик сообщения конфигурирует диалог так, чтобы показывались только файлы с расширением BMP, поскольку никаких других программа обраба- тывать не умеет. После щелчка по нужному файлу его имя появляется в раскры- вающемся списке File Name (Имя файла). Чтобы выбрать этот файл и закрыть окно, пользователь должен нажать кнопку Open (Открыть). С0ВЕТ Ад Нажатие кнопки Open не приводит к загрузке файла. При этом вызывающей про- грамме просто возвращается имя выбранного файла. Выбрав файл, пользователь загружает и выводит на экран хранящееся в нем изображение. Элементы интерфейса, позволяющие это проделать, показаны на рис. 6.3. Сначала выбирается пункт Image (Изображение) из главного меню. При этом появляется ниспадающее меню, из которого пользователь выбирает пункт Load (Загрузить). Обработчик соответствующего сообщения загружает изображение из файла в ресурс Windows. Затем пользователь выбирает пункт меню Display, и другой обработчик выводит изображение на экран, копируя пиксели из ресурса в клиентскую область окна.
152 Illi Обработка растровых изображений 1. Перейти в нужную папку 2. Щелкнуть по ВМР-файлу Select Input Image File ?|х Lookjn | >1 ImagePtocessinoProi иат ♦ I I I ifV _J Debug F«— I trip Му D ocunients My Computer Filename Т» •• ;Му Network Р... Files of type [Hardware bmp A | Bitmap Files ("bmp] F Oper>asiead-onlj Д Дрен Cancel 3. Здесь появляется имя файла Рис. 6.2. Диалог File Open 4. Нажать кнопку Open Рис. 6.3. Загрузка и вывод на экран выбранного файла 1. Выбрать пункт меню Image 2. Выбрать пункт меню Load 3. Выбрать пункт меню Display
Реализация программы 1Н1П1 153 ПРИМЕЧАНИЕ _______________________________________________ Обычно операции выбора файла, загрузки и вывода изображения совмещаются. Принятый нами подход просто упрощает отладку кода. После выполнения всех описанных действий окно будет выглядеть, как пока- зано на рис. 6.4. Рис. 6.4. Загруженное изображение выведено на экран Программа загрузки масштабирует изображение, так чтобы оно заполняло всю клиентскую область. Если размер изображения слишком велик, Windows убирает лишние пиксели. Если же его размер слишком мал, Windows добавляет пиксели, стараясь не ухудшить качества изображения. Алгоритмы добавления и удаления пикселей сохраняют аспектное отношение, поэтому пропорции изобра- жения не изменяются. На тестовой картинке, прилагаемой к программе, изображены различные ин- струменты. Загрузив ее, пользователь может выделить края каждого предмета, для чего нужно выполнить действия, описанные на рис. 6.5. Сначала из главного меню выбирается пункт Image, азатем из ниспадающего - пункт Filter. Как обычно, это действие приводит к отправке сообщения WM_COMMAND диалоговой процедуре. В ответ обработчик DlgOnCommand создает ресурс для хранения растрового изображения с выделенными краями и применяет фильтр к исходному изображению. Пиксели результирующего изоб- ражения теперь оказываются во втором ресурсе. Затем программа выводит изоб- ражение с выделенными краями в клиентскую область окна
154 IIIII Обработка растровых изображений Рис. 6.5. Применение фильтра обнаружения краев 1. Выбрать пункт меню Image 2. Выбрать пункт меню Filter СОВЕТ ______________________________________________ Если применить подход, пропагандируемый в других книгах на эту тему, то при- шлось бы сначала загрузить исходное изображение в ресурс Windows. Если же изображение с самого начала находится в ресурсе, то дополнительное копирова- ние пикселей излишне. На рис. 6.6 показано изображение, получившееся в результате применения раз- рабатываемой программы к исходному. Здесь черные пиксели соответствуют фону, а белые - краям объектов в исходном изображении. Визуально границы, форма и ориентация объектов четко распознаются. Многие программы идут дальше и на ос- нове анализа получившихся пикселей строят внутренние представления объектов. Рис. 6.6. Растровое изображение после обнаружена
Реализация программы 1И11ПН 155 ПРИМЕЧАНИЕ _______________________________________________ Описанные в этой главе функции делают некоторые предположения относитель- но того, какие данные хранятся в файле изображения. Поэтому они не будут кор- ректно работать для всех изображений. Анализ организации программы Приступая к реализации программы обработки изображений, нужно прежде всего понять, какие операции производятся над изображениями во время выпол- нения. Эти операции и будут впоследствии инкапсулированы. Во время обработки изображение подвергается различным манипуляциям, последовательность которых можно представить в виде конвейера, изображенно- го на рис. 6.7. Ресурс типа HBITMAP Ресурс типа HBITMAP Рис. 6.7. Конвейер обработки изображения Как видно из рисунка, к изображению в общем случае применяются четыре операции, которые описаны ниже: Операция Характеристика Чтение Отображение Изображение перемещается из внешней памяти во внутреннюю Изображение перемещается из внутренней памяти в буфер кадров Сохранение Изображение перемещается из буфера кадров во внутреннюю память Запись Изображение перемещается из внутренней памяти во внешнюю Можете считать эти операции жизненным циклом изображения во время рабо- ты программы. Каждая операция перемещает данные изображения из одного места в другое. Полная последовательность выглядит так: внешняя память - внутренняя память - буфер кадров - внутренняя память - внешняя память. На самом деле по- добная последовател’ чость характерна для любой структуры данных.
156 1111 Обработка растровых изображений ПРИМЕЧАНИЕ При инкапсуляции сложной структуры данных цикл «внешняя форма - внутрен- няя форма - локальная память - внутренняя форма - внешняя форма» лежит в основе всех операций и служит для автономного тестирования объекта или ме- ханизма инкапсуляции данных. При дальнейшей детализации вскрываются связи между каждой операцией и входными и выходными структурами данных: Операция Входная структура данных Выходная структура данных Чтение ВМР-файл Ресурс растрового изображения Отображение Ресурс растрового изображения Буфер кадров Сохранение Буфер кадров Ресурс растрового изображения Запись Ресурс растрового изображения ВМР-файл Каждая операция перемещает данные изображения от источника получателю. Например, операция чтения - это программный код, с помощью которого ВМР- файл преобразуется в ресурс Windows, представляющий растровое изображение. Таким образом, любая операция над изображением подразумевает перемещение данных и преобразование их в некоторую внутреннюю структуру. Когда пользователь просит вывести изображение на экран, функция Display копирует данные из ресурса в буфер кадров, точнее, в ту его часть, которая соот- ветствует физической области экрана, отображаемой на клиентскую область окна. Затем аппаратура читает пиксели из буфера кадров и строит изображение на экране. Важно отметить, что описанная схема существенно зависит от представления растрового изображения в виде ресурса Windows. В составе инкапсулированных данных хранятся описатели каждого загруженного ресурса. Выше уже отмеча- лось, что это самый эффективный способ манипулирования изображениями, по- скольку он сводит к минимуму число операций копирования пикселей. Четыре описанные операции образуют начальные требования к функцио- нальной инкапсуляции действий с растровыми изображениями. По ходу дела вы- явятся дополнительные требования, диктующие включение в механизм инкапсу- ляции новых функций. Одна из ключевых структур данных, участвующих в конвейере обработки изображения, - это BMP-файл. Определение формата BMP можно найти в до- кументации по Win32 API. Этот формат был создан специально для Windows еще со времен самых первых версий. На рис. 6.8 представлена организация ВМР-файла. В BMP-файле есть четыре основные секции. Секция BitmapFileHeader опи- сывает файл. Следующая за ним секция BitmapInfoHeader опреде • факте-
Реализация программы 1Н11МШ 157 ристики растрового изображения. Секция RGBQuadTable содержит массив структур типа RGBQUAD, в которых хранятся красная, зеленая и синяя компо- ненты каждого цвета в палитре устройства. И наконец, в секции BitmapBits запи- саны значения самих пикселей. Каждый пиксель представлен индексом палитры, находящейся в секции RGBQuadTable. Все эти секции подробно описаны в документации по Win32 API. Приводить полное описание здесь было бы долго, поэтому мы остановимся только на самых важных моментах. Поле fbType в секции BitmapFileHeader содержит строку «ВМ» (очевидно, сокращение от «bitmap»). Также представляет интерес поле bfOffBits. Это смещение в байтах от конца структуры BITMAPFILEHEADER до начала секции BitmapBits в файле. В секции BitmapInfoHeader есть несколько важных полей. Поле biWidth опре- деляет ширину изображения в пикселях, а поле biHeight - его высоту. Поле biHeight может быть как положительным, так и отрицательным. Если оно поло- жительно, то пиксели расположены в растре снизу вверх, и началом изображения считается нижний левый угол. Отрицательное значение означает, что пиксели идут сверху вниз и началом является верхний левый угол. Обычно программа должна изменить порядок строк изображения, хранящегося в формате «сверху вниз», перед тем как выводить его на экран. WORD bfType DWORD bfSize BitmapFileHeader - ► WORD bf Reserved 1 WORD bfReserved2 DWORD bfOffBits DWORD bfSize LONG biWidht LONG biHeight WORD biPlanes WORD biBitCount ОЦГПартТОПваивГ ~ F RGBQuadTable (Red, Green, Blue, Flog) • • • (Red, Green, Blue, Flog) DWORD biCompression DWORD biSizeimage LONG biXPelsPerMeter BitmapBits LONG biYPelsPerMeter DWORD biCIrUsed DWORD biCIrlmportant Рис. 6.8. Формат BMP-файла в Windows
158 1111 Обработка растровых изображений MUTt’I со вет AjW Если загрузить изображение прямо в ресурс Windows, то система сама разберет файл и выполнит все необходимые манипуляции с пикселями. В секции BitmapInfoHeader есть еще два важных поля: biClrUsed и biClr- Important. Они определяют число элементов палитры в таблице RGBQuadTable. Если они заданы неправильно, то приложение может повести себя странно и даже аварийно завершиться. Так, если значение больше, чем истинное число цветов в палитре, то Windows интерпретирует пиксели как RGB-цвета. Когда дело дой- дет до вывода на экран, получившиеся некорректные цвета приведут к искаже- нию изображения. Каждый элемент таблицы RGBQuadTable - это 32-разрядное число, пред- ставляющее сочетание красного, зеленого и синего цветов. Оно называется «RGB-четверкой». Слово «четверка» подразумевает, что полное значение разби- то на четыре части, по 8 битов в каждой. Ясно, что первые три соответствуют до- лям красного, зеленого и синего. Последняя часть зарезервирована и должна быть равна нулю. Поскольку для представления доли цвета используется 8 битов, то значение изменяется от 0 (отсутствие данного цвета) до 255 (максимальная доля). Каждому пикселю прямоугольного изображения соответствует одно число в секции BitmapBits. Числа записаны построчно: сначала все пиксели строки О, затем все пиксели строки 1 и т. д. Число представляет собой индекс в таблицу RGBQuadTable. Если таблица состоит из 256 записей, то значение пикселя изме- няется от 0 до 255. На рис. 6.8 поле bClrUsed объявлено как DWORD. В файле windef.h этот тип определен следующим образом: typedef unsigned long DWORD; Иными словами, DWORD - это 32-разрядное целое без знака и, следователь- но, изменяется в диапазоне от 0 до 4 294 967 295. Стало быть, с помощью таблицы RGBQuadTable можно представить около 4 млрд комбинаций красного, зеленого и синего. Мы уже отмечали, что четыре базовые операции над растровыми изображени- ями должны лечь в основу механизма инкапсуляции. Соответствующие функции вызывают различные функции Win32 API для выполнения нужного действия. На рис. 6.9 показано соотношение между функциями из компонента BitmapUtilities и функциями Win32 API. В левом верхнем углу вы видите инкапсулированные функции, входящие в состав BitmapUtilities, а справа - соответствующие им функции Win32 API. Каждая функция из BitmapUtilities обращается к одной функции API, эти пары соединены линией. В состав компонента BitmapUtilities входят и другие функ- ции, и в реализации некоторых из них участвуют несколько функций Win32 API. Но на рис. 6.9 показаны самые важные. Рассмотрим, например, функцию DisplayABitmap, принадлежащую Bitmap- Utilities. Она вызывает функцию StretchBlt из Win32 API, передавая ей два аргу- мента. Один из них на рисунке назван <WindowsDC/Bitmap>. Из этого обозначе-
Реализация программы 1Н11ПН 159 ния следует, что StretchBlt копирует изображение из одного контекста устрой- ства в другой. Рис. 6.9. Соответствие между инкапсулированными вспомогательными функциями и Win32 API ПРИМЕЧАНИЕ Напомним, что контекст устройства-это набор инструментов рисования. Одним из них, ранее не упоминавшимся, является ресурс, представляющий растровое изображение. Итак, StretchBlt перемещает изображение, привязанное к исходному контек- сту устройства, в целевой контекст. Понятно, что гораздо проще было бы напря- мую скопировать исходное изображение в целевое. Но поскольку Microsoft реши- ла, что все инструменты рисования должны находиться в одной структуре - контексте устройства, то надо быть последовательными и применить такой же подход к работе с изображениями. Внутри DisplayABitmap создается контекст устройства в памяти, и к нему присоединяется исходное изображение. Затем с по- мощью StretchBlt это изображение копируется из контекста в памяти в контекст,
160 III! Обработка растровых изображений связанный с клиентской областью. Таким образом, требуется вызывать еще и дру- гие функции API, помимо StretchBlt, и лучше скрыть всю эту деятельность в од- ной функции, к которой можно обращаться из любого места в программе. Из рис. 6.9 видно, что в каждом вызове функции Win32 API явно или неявно участвует ресурс Windows, представляющий растровое изображение. Это не долж- но вызывать удивления, ведь мы подчеркивали выше, что наша реализация опи- рается на ресурсы. Две функции - ReadABitmapFromAFile и DumpABitmap - по- лучают описатели ресурсов в виде значений, возвращаемых функциями API, а две другие - DisplayABitmap и WriteABitmapToAFile - в виде входных аргументов. Включение функций из компонента BitmapUtilities в программу не вызывает никаких сложностей. Собственно, для реализации каждой операции пользова- тельского интерфейса нужно в обработчике соответствующей команды всего лишь объявить несколько переменных и вызвать единственную функцию. Реализация программы обработки изображений Прежде всего нам предстоит реализовать вспомогательные функции низкого уровня, которыми мы потом воспользуемся в программе. Последовательность разработки снизу вверх выглядит следующим образом. 1. Реализовать компонент FileNameMgr для работы с диалогами File Open и File Save. 2. Реализовать компонент KernelMgr, относящийся к фильтру для обнаруже- ния краев. 3. Разработать компонент BitmapUtilities для выполнения базовых операций над растровыми изображениями и применения к ним фильтра. 4. Добавить в меню необходимые пункты и связать с ними ниспадающие меню. 5. Модифицировать диалоговую процедуру DlgProc и обработчик сообщения DlgOnCommand, включив в них функции из компонента BitmapUtilities. 6. Модифицировать обработчик DlgOnPaint для вывода изображения на экран. В оставшейся части раздела мы проанализируем этот перечень и опишем со- ответствующий код. Реализация компонента FileNameMgr Имя файла приходится выбирать во многих программах. Поэтому в Win32 API есть два диалога, специально предназначенных для этой цели: GetOpenFileName и GetSaveFileName. Для их использования нужно заполнить некоторую структу- ру, а затем извлечь из нее заданное пользователем имя. Поскольку эти операции выполняются часто, удобно было бы сделать работу с диалогами максимально простой. Мы предоставим две функции, по одной для каждого диалога. Каждая из них заполнит структуру значениями, часть из кото- рых передается вызывающей программой, а часть выбирается по умолчанию. Эти функции очень похожи, поэтому мы рассмотрим только ту, которая ин- капсулирует диалог GetOpenFileName. Вот ее объявление
Реализация программы IIIIIIIH 161 void GetlnputFileName(TCHAR * * FileName, int NumberChars, TCHAR * Filterstring) ; Как видите, эта функция позволяет очень легко получить имя открываемого файла. Первым аргументом ей передается FileName - адрес буфера, в который долж- но быть помещено имя файла. Далее следует размер буфера - NumberOfChars. И по- следний аргумент - строка фильтра, FilterString - определяет перечень расшире- ний файлов, показываемых в окне обозревателя. Строки фильтра записываются в формате, диктуемом функцией GetOpen- FileName, например: _tscpy( Filterstring, _TEXT("Bitmap Files (*.bmp)]*.bmp|") ) ; Фильтр содержит пару строк для каждого расширения. Строки разделяются символом «|». Первая строка в паре отображается в раскрывающемся списке Туре (Тип файлов), который присутствует в диалоговом окне. Вторая строка использу- ется при отборе файлов, показываемых в окне обозревателя. СОВЕТ Разделитель «/«должен присутствовать даже после самой последней строки, ука- занной в фильтре. В противном случае при инициализации диалога произойдет ошибка. Функция GetlnputFileName заполняет структуру переданными аргументами и значениями, выбранными по умолчанию, а затем вызывает стандартный диалог GetOpenFileName. /*,********************************************* * File: FileNameMgr.с ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ void GetlnputFileName(TCHAR * FileName, int NumberChars, TCHAR * Filterstring) { OPENFILENAME FileData ; ReformatFilterString(Filterstring) ; FileData.1structsize FileData.hwndOwner FileData-hlnstance FileData.IpstrFilter FileData.IpstrCustomFilter FileData.nMaxCustFilter FileData.nFiIterIndex FileData.IpstrFile FileData.nMaxFile FileData.IpstrFileTitle FileData.nMaxFileTitle = sizeof(OPENFILENAME) ; = NULL ; = NULL ; = Filterstring ; = NULL ; = 0 ; = 1 ; = FileName ; = NumberChars ; = NULL ; = 0 ;
162 Illi Обработка растровых изображений FileData . IpstrlnitialDir = NULL ; FileData.IpstrTitle = TEXT("Select Input Image File") ; FileData.Flags = OFN_PATHMUSTEXIST 1 OFN_FILEMUSTEXIST 1 OFN_LONGNAMES ; FileData.nFileOffset = 0 ; FileData.nFileExtension = 0 ; FileData.IpstrDefExt = NULL ; FileData.ICustData = 0 ; FileData.IpfnHook = NULL ; FileData.IpTemplateName = NULL ; GetOpenFileName(SFileData) ; ) Чтобы воспользоваться стандартным диалогом, нужно сначала подготовить структуру FileData типа OPENFILENAME. Перед тем как передавать строку фильтра диалогу, мы преобразуем ее в нужный формат с помощью вспомогатель- ной функции ReformatFilterString. Вызывающая программа разделяет строки символом «|», поскольку его легко набрать на клавиатуре. Но на самом деле стан- дартный диалог ожидает, что строки будут завершаться символом «\0», то есть обычным символом конца строки. Функция ReformatFilterString как раз и заме- няет один символ другим. Для многих полей выбраны умалчиваемые значения, к примеру 0 или NULL. Их смысл подробно описан в документации. Порядок использования переданных аргументов очевиден и не нуждается в пояснениях. Но некоторые из прочих по- лей заслуживают дополнительного обсуждения. В поле IStructSize должен быть занесен размер структуры OPENFILENAME в байтах. Он вычисляется с помощью оператора sizeof. По значению в этом поле Windows определяет версию структуры. Если его не инициализировать, диалог не откроется. В значении поля Flags участвует символическая константа OFNLONG- NAMES. Она говорит, что нужно выводить длинные имена файлов. Не включи мы этот флаг, обозреватель показывал бы имена в формате DOS 8.3. Заполнив структуру типа OPENFILENAME, функция передает ее адрес стан- дартному диалогу GetOpenFileName. Затем пользователь с помощью мыши и нави- гационной панели сможет выбрать имя файла. После возврата из GetOpenFileName выбранное имя окажется в поле FileName, поскольку инкапсулирующая функция поместила его адрес в поле IpstrFile структуры OPENFILENAME. ПРИМЕЧАНИЕ Стандартный диалог GetOpenFileName только возвращает выбранное пользова- телем имя файла, но не открывает его. Компонент KernelMgr Фильтр преобразует одно изображение в другое, заменяя пиксели. Фильтр представляет собой небольшую матрицу коэффициентов, которою программа
Реализация программы 163 применяет к соседним пикселям. Обычно размер матрицы для простого фильтра равен 3 х 3. В теории обработки изображений эту матрицу называют ядром. Рассматриваемый компонент предназначен для управления матрицей коэффи- циентов фильтра размером 3x3. Вот объявления входящих в его состав функций: /*********************************************** ★ * File: KernelMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ void InitializeKernel(void) ; void GetNumberRowsAndCols(int * Rows, int * Cols) ; int GetScaleFactor(void) ; int GetThresholdValue(void) ; int GetKernelCoefficient( int Row, int Col ) ; Помимо матрицы коэффициентов, компонент KernelMgr определяет также коэффициент масштабирования и пороговое значение. Коэффициент масштаби- рования служит для приведения результатов применения ядра к подходящему диапазону значений. А пороговое значение определяет, какие из вырабатываемых фильтром пикселей следует считать черными, а какие - белыми. СОВЕТ Способ применения фильтра будет описан при рассмотрении компонента BitmapUtilities ниже. Когда BitmapUtilities вызывает функцию InitializeKernel, матрица инициа- лизируется следующими значениями: -1 -1 -1 -1 8 -1 -1 -1 -1 Это ядро приписывает максимальный вес центральному пикселю, а окружаю- щим его пикселям - меньшие веса. Во многих книгах, посвященных обработке изображений, утверждается, что этот фильтр наиболее эффективен для обнару- жения краев. Разработка компонента BitmapUtilities В состав этого компонента входят функции для простых и эффективных ма- нипуляций растровыми изображениями, описанных выше на рис. 6.7. Первой в конвейере выполняется функция, которая читает изображение из файла в ресурс. Ниже показана ее реализация. /*********************************************** * File: BitmapUtilities .с
164 Illi Обработка растровых изображений * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ***********************************************/ void ReadABitmapFromAFile(TCHAR * FileName, HDC DeviceContext, HBITMAP * Bitmap ) BITMAPFILEHEADER FileHeader ; BITMAPINFOHEADER BitmapHeader ; RGBQUAD * BitmapRGB ; int Size ; BYTE * BitmapBits ; HANDLE File ; DWORD BytesRead ; BOOL Status ; HBITMAP WorkingBitmap ; int RGBSize ; BITMAPINFO * Bitmapinfo ; BYTE * RGBStart ; File = CreateFile( FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ) ; Status = ReadFile(File, &FileHeader, sizeof(BITMAPFILEHEADER), SBytesRead, NULL) ; Status = ReadFile(File, SBitmapHeader, sizeof(BITMAPINFOHEADER), SBytesRead, NULL) ; RGBSize = BitmapHeader.biClrUsed * sizeof(RGBQUAD) ; BitmapRGB = (RGBQUAD *) malloc( RGBSize ) ; Status = ReadFile(File,BitmapRGB, RGBSize ,SBytesRead,NULL) ; Bitmapinfo = (BITMAPINFO *) malloc(sizeof(BITMAPINFOHEADER) + RGBSize) ; memcpyl Bitmapinfo, SBitmapHeader, sizeof(BITMAPINFOHEADER) ) ; RGBStart = (BYTE *)Bitmapinfo + sizeof(BITMAPINFOHEADER) ; memcpyl RGBStart,BitmapRGB,RGBSize) ; WorkingBitmap = CreateDIBSection(DeviceContext, (PBITMAPINFO)Bitmapinfo, DIB_RGB_COLORS, SBitmapBits, NULL, 0) ; Size = BitmapHeader.biWidth * BitmapHeader.biHeight * sizeof(BYTE) ; Status = ReadFile(File, BitmapBits, Size, SBytesRead,NULL) ; free(Bitmapinfo) ; free(BitmapRGB) ; CloseHandle(File) ; Bitmap = WorkingBitmap ;
Реализация программы »!! 165 Эта функция обрабатывает данные из BMP-файла согласно описанию форма- та, приведенному на рис. 6.8. Поскольку файл состоит из четырех секций, то функ- ция Win32 API ReadFile вызывается ровно четыре раза. Для открытия файла мы пользуемся функцией CreateFile. Она принимает имя файла и много других параметров. Но для большинства из них задаются зна- чения по умолчанию, интересующиеся читатели могут найти их описание в до- кументации. Эта функция возвращает описатель файла, который мы сохраняем в переменной File. При чтении из файла описатель передается функции ReadFile в качестве первого аргумента. Кроме того, ReadFile получает адрес буфера, в кото- рый следует поместить прочитанные байты, например, FileHeader, и число под- лежащих чтению байтов. Она считывает байты из файла, начиная с текущей по- зиции, и смещает эту позицию так, чтобы она указывала на первый еще не прочитанный байт. По завершении всех операций чтения описатель File передает- ся функции CloseFile, которая закрывает файл. Секции FileHeader и BitmapHeader - это структуры фиксированного размера, поэтому для считывания каждой из них достаточно одного вызова ReadFile. Для чтения же таблицы цветов нужна дополнительная обработка. В каждом ВМР- файле есть своя таблица цветов. Мы вычисляем размер таблицы и записываем его в переменную RGBSise, выделяем память для ее хранения (BitmapRGB), а затем считываем в эту память всю таблицу. Прежде чем считывать сами пиксели, функция создает ресурс, представляю- щий растровое изображение. Это позволяет избежать считывания пикселей сна- чала в локальную память, а затем в ресурс. Экономия вызвана тем, что функция создания ресурса возвращает как его описатель, так и адрес области памяти, выде- ленной для хранения пикселей. Зная адрес, ReadFile считывает значения пиксе- лей непосредственно в эту область, обходясь без промежуточного копирования. Создание ресурса - двухшаговая процедура. В переменной Bitmapinfo типа BITMAPINFO хранится содержимое структур BitmapHeader и BitmapRGB, кото- рые были инициализированы ранее. Она полностью описывает изображение и несет информацию о палитре, необходимую Windows для правильной интерпре- тации пикселей. Инициализировав эту структуру, программа передает ее функ- ции Win32 API CreateDIBSection, которая возвращает описатель ресурса в пере- менной WorkingBitmap и адрес BitmapBits области памяти достаточного для хранения всех пикселей размера. Теперь можно вызвать функцию ReadFile для чтения значений пикселей пря- мо по адресу BitmapBits. Закончив загрузку данных, мы возвращаем описатель ресурса WorkingBitmap вызывающей программе. После копирования BMP-файла в ресурс Windows мы можем вывести его це- ликом или частично в клиентскую область окна. Этим занимается функция DisplayABitmap, код которой приведен ниже. void DisplayABitmap(HDC DeviceContext, HBITMAP Bitmap, int DstXStart, int DstYStart, int DstWidth, int QstHeight , int SrcXStart, int SrcYStart, int SrcWidth, int SrcHeight )
166 III! Обработка растровых изображений { HDC InMemoгуDC ; HBITMAP OldBitmap ; InMemoryDC = CreateCompatibleDC(DeviceContext) ; OldBitmap = Selectobject(InMemoryDC,Bitmap) ; StretchBlt(DeviceContext,DstXStart, DstYStart, DstWidth, DstHeight, InMemoryDC,SrcXStart,SrcYStart,SrcWidth,SrcHeight, SRCCOPY) ; SelectObj ect(InMemoryDC,OldBitmap) ; DeleteDC(InMemoryDC) ; } В качестве первого аргумента эта функция получает целевой контекст устрой- ства DeviceContext. С ним ассоциировано некоторое растровое изображение. За- дача функции - подменить его изображением Bitmap, описатель которого пере- дан во втором аргументе. Остальные аргументы описывают участки исходного и целевого изображений, подлежащие копированию. Для копирования растровых изображений в Win32 API есть несколько функ- ций. В DisplayABitmap мы воспользовались функцией StretchBlt, а в других функ- циях из компонента BitmapUtilities применим Bit Bit. StretchBlt - это наиболее гиб- кий способ, она позволяет скопировать часть исходного изображения в часть целевого. При этом выбранная часть исходного изображения сжимается или растя- гивается, так чтобы целиком заполнить указанную часть целевого изображения. К сожалению, с использованием любой из вышеупомянутых функций связана одна тонкость. Они ссылаются не на сами изображения, а на контексты устройств, к которым эти изображения привязаны. Но исходное изображение изначально ни с чем не связано, поэтому приходится завести для него временный контекст в памяти. С помощью функции Win32 API CreateCompatibleDC программа создает кон- текст устройства в памяти InMemoryDC. Затем вызывается функция Select- Object, которая привязывает исходное изображение Bitmap к этому контексту. В качестве возвращаемого значения программа получает описатель ранее привя- занного к контексту изображения и сохраняет его в локальной переменной Old- Bitmap. Теперь можно вызывать StretchBlt, передав ей в качестве исходного кон- текста InMemoryDC, а в качестве целевого - DeviceContext. Если DeviceContext соответствует клиентской области диалогового окна, то в результате этой опера- ции изображение появится в клиентской области. * Покончив с копированием, программа возвращает изображение OldBitmap в контекст InMemoryDC, снова вызывая SelectObject. После этого контекст InMemoryDC можно освободить с помощью функции DeleteDC. СОВЕТ Как и во многих других случаях, имя DeleteDC неточно описывает назначена функции. Число контекстов устройств в Windows фиксировано, поэтому DeleteDC не уничтожает контекст, а лишь возвращает его системе. Основное достоинство функции DisplayABitmap в том, что она загружает ВМР-файл непосредственно в ресурс Windows. После этого лтя вывела изобра-
Реализация программы HIIIMBII 167 жения на экран нужно всего несколько строк - пять, если быть точным. Для со- хранения изображения в функции DumpABitmap (ее текст здесь не приводится, но его можно найти на сайте http://www.osborne.com) принят аналогичный под- ход. Правда, пришлось проделать дополнительную работу по построению за- головка изображения и таблицы цветов. Но коль скоро это сделано, для сохране- ния растрового изображения, привязанного к контексту, который ассоциирован с клиентской областью окна, нужно всего шесть строк кода. Простой алгоритм обнаружения краев реализован в функции ApplyKernel- ToBitmap, тоже входящей в состав BitmapUtilities. Она отнесена именно к этому компоненту, поскольку выполняет операции над ресурсом, представляющим рас- тровое изображение, a BitmapUtilities как раз и инкапсулирует все такие операции. Полный исходный текст этой функции имеется на сайте, здесь же приведем только ее прототип: void ApplyKernelToBitmap( HDC DeviceContext, HBITMAP Bitmap) У этой функции два аргумента. Первый, DeviceContext, описывает контекст устройства, содержащего изображение, к которому применяется фильтр. Во вто- ром аргументе передается описатель исходного изображения Bitmap. Он служит одновременно исходным и целевым изображением. В ходе применения фильтра функция берет пиксели из Bitmap, накладывает на локальную копию пикселей матрицу ядра и создает выходные пиксели. Затем получившиеся пиксели копиру- ются назад в Bitmap. Чтобы вывести результат на экран, программа должна выз- вать функцию DisplayABitmap, передав ей профильтрованное изображение. Получение указателя на массив исходных пикселей, хранящихся в ресурсе Bitmap, - самый эффективный способ доступа к ним. В этом случае функция ApplyKernelToBitmap может напрямую работать с областью памяти, где пиксели хранятся, а не копировать их в локальную память. На псевдокоде применение фильтра обнаружения краев описывается так: получить указатель на массив пикселей в ресурсе, представляющем растровое изображение ; извлечь параметры ядра фильтра ; вычислить нормировочный множитель, равный сумме всех коэффициентов ядра ; для 'каждой строки изображения выполнить begin для каждой колонки изображения выполнить begin вычислить взвешенную сумму пикселей, находящихся в прямоугольнике того же размера, что и матрица ядра, с левым верхним углом в точке (строка, колонка) ; разделить результат на нормировочный множитель ; если результат меньше порогового значения приравнять пиксель нулю ; записать результат в позицию (строка, колонка) выходного массива end end скопировать выходной массив пикселей в исходный ; Можно представлять себе применение ядра как продвижение небольшой мат- рицы коэффициентов по массиву исходных пикселей. На каждом шаге вычисляет-
168 ПН Обработка растровых изображений ется линейная комбинация коэффициентов ядра и пикселей, оказавшихся в обла- сти перекрытия. Получающееся число подвергается нормализации, сравнивается с порогом, а затем становится выходным пикселем, который заменяет тот, что на- ходился в левом верхнем углу области перекрытия. Сравнив псевдокод с фактическим кодом функции ApplyKernelToBitmap, чита- тель легко разберется в реализации. Для корректной работы алгоритма надо учесть еще ряд мелких деталей, и в реальной программе это сделано. Подробное описание всех тонкостей можно найти в любом учебнике по обработке изображений. Добавление меню Добавить пункты главного меню, ниспадающие меню и их пункты позволяет редактор меню, встроенный в Visual C++ или Embedded Visual C++. Поскольку мы уже добавляли в программу пункт меню Quit, эта задача не вызовет у вас сложностей. Процедура включения новых элементов в существующее меню описана в гла- ве 7. Поэтому сейчас останавливаться на работе с редактором мы не будем, это отвлекло бы нас от основной цели настоящей главы - обработки изображений. Модификация функций DlgProc и DlgOnCommand Чтобы связать выбор пользователем пункта меню с выполнением конкретных операций, нам придется объявить в диалоговой процедуре несколько переменных и воспользоваться ранее разработанными инкапсулированными функциями,- Объявления переменных находятся в начале файла DlgProc.c: TCHAR InputFileName[256] ; TCHAR Filterstring[256] ; static HBITMAP LoadedBitmap = NULL ; static BOOL BitmapLoaded = FALSE ; В массиве InputFileName мы сохраним имя выбранного пользователем файла. Строка фильтра FilterString инициализируется в функции OnlnitDialog так, что- бы при вызове GetlnputFileName показывались только файлы с расширением BMP. В переменной LoadedBitmap будет сохранен описатель ресурса, в который мы загрузим файл. Булевская переменная BitmapLoaded управляет исполнением кода вывода изображения в обработчике DlgOnPaint. Обработка команд пользователя происходит в обработчике DlgOnCommand сообщения WM_COMMAND, где все эти переменные и используются. Ниже приведен его текст: void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { HINSTANCE Instance ; HDC DeviceContext ; int Clientwidth ; int ClientHeight ; Instance = GetProgramlnstance() ; switch( ilD )
Реализация программы И11ПШН 169 { case ID_SELECT_INPUT: GetlnputFileName(InputFileName, 256, Filterstring) ; break ; case ID_SELECT_OUTPUT: GetOutputFileName(OutputFileName, 256, Filterstring) ; break ; case ID_IMAGE_LOAD: if ( _tcscmp(InputFileName,__TEXTf")) != 0 ) { DeviceContext = GetDC(hDlg) ; ReadABitmapFromAFile(InputFileName, DeviceContext, SLoadedBitmap ) ; BitmapLoaded = TRUE ; ReleaseDC(hDlg,DeviceContext) ; ) break ; case ID_IMAGE_DISPLAY: InvalidateRect(hDlg,NULL,TRUE) ; UpdateWindow(hDlg) ; break ; // дальнейший код для краткости опущен } } Здесь показана только часть обработчика WM_COMMAND, а именно ветви, относящиеся к выбору BMP-файла, его загрузке в ресурс Windows и выводу изоб- ражения на экран. Раньше мы получали контекст устройства от функции BeginPaint, вызываемой из обработчика DlgOnPaint. А в этом примере, точнее, в ветви IDIMAGEJLOAD, мы видим, что программа может получить его и иначе - обратившись к функции GetDC, которой нужно передать описатель окна. В результате будет возвращен контекст, содержащий все инструменты, связанные с данным окном. Закончив работать с этим контекстом, нужно вызвать функцию ReleaseDC, передав ей тот же самый описатель, и она вернет контекст системе. В ветви ID_IMAGE_LOAD изображение загружается из файла InputFile- Name. Для этого мы пользуемся функцией ReadABitmapFromAFile, входящей в состав инкапсулированного компонента BitmapUtilities. Получив описатель ре- сурса, в который загружен файл, мы устанавливаем флаг BitmapLoaded в TRUE. Он анализируется в обработчике DlgOnPaint: если файл загружен, то любая пере- рисовка клиентской области сводится к выводу на экран изображения из ресурса LoadedBitmap. Если пользователь выбрал из меню пункт Image > Display, то управление по- падает в ветвь ID_IMAGE_DISPLAY. В ответ обработчик вызывает пару функ- ций InvalidateRect и UpdateWindow, что приводит к немедленной перерисовке ' лиентской области. Мы это уже не раз обсуждали.
170 ВНИМШН Обработка растровых изображений Ж"Т*| СОВЕТ рЕр В большинстве программ выбор файла, его загрузка в ресурс и вывод изображе- g=J ния на экран - это одна неделимая операция. Но для иллюстрации идей и упро- ------ щения тестирования мы разбили эту последовательность на отдельные шаги. Модификация обработчика DlgOnPaint Копирование загруженного изображения в клиентскую область возлагается на обработчик DlgOnPaint. Вот та его часть, которая непосредственно относится к решению этой задачи: void DlgOnPaint(HWND hDlg) { // Строки, рассмотренные выше, опущены if (BitmapLoaded) { GetClientDimensions( hDlg, SClientWidth, SClientHeight ) ; GetBitmapDimensions(LoadedBitmap,SBitmapWidth, SBitmapHeight) ; DisplayABitmap(DeviceContext, LoadedBitmap, 0, MENU_OFFSET, Clientwidth, ClientHeight, 0, 0, Bitmapwidth, BitmapHeight ) ; } // Строки, рассмотренные выше, опущены } По крайней мере, один раз этот обработчик выполняется еще до того, как пользователь выберет какой-нибудь пункт меню. Но пока изображение не загру- жено, не надо пытаться вывести его. До момента загрузки флаг BitmapLoaded ра- вен FALSE. Проверка этой переменной позволяет обработчику обойти код отобра- жения. Мы уже видели выше, что флаг BitmapLoaded устанавливается в TRUE после того, как имя файла выбрано, и он загружен в ресурс с описателем LoadedBitmap. Коль скоро переменная BitmapLoaded равна TRUE, обработчик может выпол- нить охраняемый ей фрагмент кода. Он получает от функции GetClientDimensions (она принадлежит компоненту BitmapUtilities, но не обсуждалась в этой главе) размеры клиентской области, а от функции GetBitmapDimensions (тоже входя- щей в состав BitmapUtilities) - размеры загруженного изображения. Зная то и другое, можно вызвать функцию DisplayABitmap, которая выведет изображение из ресурса LoadedBitmap в окно, представленное контекстом устройства DeviceContext. ПРИМЕЧАНИЕ .. ................ тг » ...I । г.... Функция StretchBIt, вызываемая из DisplayABitmap, автоматически масштаби- рует изображение LoadedBitmap на размеры клиентской области. Так, файл CACTUS.BMP, прилагаемый к данной программе, содержит изображение разме- ром 640x480 пикселей, но оно прекрасно смотрится на экране КПК. В предыдущем фрагменте константа MENU_OFFSET передана в качестве ординаты левого верхнего угла области, в которую выводится изображение. Она
Разработка заставки ШИМ 171 необходима из-за семантических различий между версиями функции Get- ChentRect для настольного ПК и КПК. Функция GetClientDimensions, входящая в состав компонента BitmapUtilities, обращается к GetClientRect, чтобы опреде- лить размеры клиентской области окна. Для настольного ПК клиентская область начинается под полосой главного меню, а для КПК включает эту полосу. Поэтому приходится программно компенсировать такое различие. ПРИМЕЧАНИЕ Если не скорректировать область вывода в программе, работающей на КПК, то главное меню перекроет часть изображения. Разработка заставки с помощью функций из файла BitmapUtilities Пользуясь функциями, инкапсулированными в компонент BitmapUtilities, мож- но включить в программу ряд интересных возможностей. Одной из них является заставка. В начальный момент в клиентскую область окна выводится некоторое из- ображение, которое часто содержит логотип компании и продукта. Спустя короткое время изображение исчезает и заменяется нормальным интерфейсом программы. Описание пользовательского интерфейса программы Поскольку наша задача - проиллюстрировать применение компонента BitmapUtilities для быстрой реализации заставки, то пользовательский интер- фейс программы будет совсем простым. Сразу после загрузки окно программы выглядит, как показано на рис. 6.10. Рис. 6.10. Вывод заставки
172 1111 Обработка растровых изображений Заставка для этой программы - цветная фотография знаменитого моста «Зо- лотые ворота» и панорамы Сан-Франциско. На рисунке видно, что она заполняет всю клиентскую область и расположена ниже меню. Спустя несколько секунд заставка исчезает и появляется обычный пользова- тельский интерфейс, показанный на рис. 6.11. В клиентскую область выводится строка с названием этой книги, и дальше она уже не меняется. Что бы пользователь ни делал, заставка не появится. Splash Screen Program Pocket PC Developer's Guide Рис. 6.11. После исчезновения заставки Описание внутренней работы программы В любой Windows-программе модель рисования разделяет обновление дан- ных и окна. На рис. 6.12 представлена модель рисования для этой программы. Цифры на рисунке описывают последовательность событий, приводящих к появлению и исчезновению заставки. 1. Пользователь запускает программу. Она загружается в память и получает сообщение WM_INITDIALOG. 2. В обработчике OnlnitDialog программа загружает BMP-файл в ресурс SplashBitmap, вызывая функцию Read ABitmapFile, и взводит таймер. 3. Загрузив изображение, обработчик OnlnitDialog поднимает флаг Splash- BitmapLoaded, показывающий, что изображение можно выводить. 4. В процессе инициализации Windows автоматически вызывает обработчик DlgOnPaint сообщения WM_PAINT. Он проверяет состояние флага Splash- BitmapLoaded, видит, что изображение загружено, и выводит его на экран. Для этого применяется функция DisplayABitmap из компонента Bitmap- Utilities. 5. Заставка появляется на экране, заполняя всю клиентскую область ниже полосы меню.
Разработка заставки ПИП 173 6. Спустя некоторое время срабатывает таймер, и диалоговая процедура по- лучает сообщение WM_TIMER. 5. 10. Заставка выводится на экран Выводится нормальный пользовательский интерфейс 1 .{WMJNITDIALOG}^' 3\\ Сохраненные данныеХ^м SplasBitmap-^ - SplasBitmapLoaded-----> 4.(wM PAINT] .--------..............."* 9. Рисовать с использованием 6.НЛ/М_Г1Му----- сохраненных данных о. Сохраненные данные InvalidateRect, UpdateWindow Рис. 6.12. Модель рисования заставки 7. В этом обработчике таймер останавливается, поскольку больше события от него не нужны. Затем флаг SplashBitmapLoaded сбрасывается, это слу- жит признаком того, что больше выводить заставку не следует. 8. Далее обработчик вызывает функции InvalidateRect и UpdateWindow, что приводит к перерисовке клиентской области. Аргументы InvalidateRect за- даются так, чтобы вся клиентская область оказалась закрашенной цветом фона. 9. Обработчик DlgOnPaint видит, что флаг SplashBitmapLoaded сброшен, и переходит на ветвь, где рисуется нормальный пользовательский интер- фейс, то есть строка, показанная на рис. 6.11. Внимательное рассмотрение этой последовательности действий выявляет ин- тересный факт. Вся функциональность, необходимая для вывода заставки, сосре- доточена исключительно в диалоговой процедуре и обработчиках сообщений. ПРИМЕЧАНИЕ Повторно используя функции из компонента BitmapUtilities, разработчик без тру- да может включить в программу новую возможность. Для этого необходимы толь- ко две функции: ReadABitmapFromFile и DisplayABitmap. Реализация программы вывода заставки Как уже отмечалось, все изменения, необходимые для добавления заставки, сосредоточены в диалоговой процедуре DlgProc и обработчиках некоторых сооб- щений. Вот что нужно сделать для вывода заставки.
174 1111 Обработка растровых изображений 1. Добавить в файл DlgProc.c обработчик сообщения WM_TIMER. 2. Объявить переменные и константы, необходимые для поддержки ресурса, представляющего изображение, управления его выводом и работы с тай- мером. 3. Модифицировать обработчик OnlnitDialog, так чтобы он загружал застав- ку из BMP-файла, поднимал флаг и взводил таймер. 4. В обработчике DlgOnTimer остановить таймер, сбросить флаг, управляю- щий выводом заставки, и перерисовать клиентскую область. 5. В обработчик DlgOnPaint включить проверку флага и в зависимости от его значения рисовать либо заставку, либо обычный интерфейс. Изменения касаются только самой процедуры DlgOnProc и обработчиков OnlnitDialog, DlgOnTimer, DlgOnPaint. Ниже показано, как все это реализуется с применением компонента BitmapUtilities. Добавление обработчика сообщения WM_TIMER Как всегда, добавление обработчика производится в три этапа. Сначала нуж- но объявить функцию DlgOnTimer, затем добавить в предложение switch анализа- тор HANDLE_DLG_MSG и, наконец, включить в файл DlgProc.c тело функции. Все эти фрагменты поможет сгенерировать мастер Message Cracker Wizard. Объявление переменных и констант В файле DlgProc.c нужно объявить две переменные, управляющие выводом заставки, и две константы, необходимые для работы с таймером. HBITMAP SplashBitmap ; BOOL SplashBitmapLoaded = FALSE ; ♦define TIMER_ID 100 ♦define DISPLAY_PERIOD 5000 В переменной SplashBitmap мы будем хранить описатель ресурса, в который загружено изображение. А флаг SplashBitmapLoaded управляет выводом этого ресурса на экран. Константа TIMER_ID - это уникальный идентификатор таймера, управляю- щего выводом заставки. Через DISPLAY_PERIOD миллисекунд заставка исчеза- ет и заменяется обычным интерфейсом. Модификация обработчика OnlnitDialog Сразу после запуска программы Windows отправляет диалоговой процедуре сообщение WM INITDIALOG. В обработчике и происходит вся инициализация. BOOL OnlnitDialog ( HWND hDlg , HWND hDlgFocus , long UnitParam ) { HDC DeviceContext ; // Строки, рассмотренные ранее, опущены DeviceContext = GetDC(hDlg) ; ReadABitmapFromAFile(_TEXT("bridge.bmp"),DeviceContext,&SplashBitmap ) . ReleaseDC(hDlg,DeviceContext) ; SplashBitmapLoaded = TRUE ;
Разработка заставки НИВ 175 SetTimer(hDlg,TIMER_ID,DISPLAY_PERIOD,NULL) ; return TRUE ; ) Здесь выполняются три существенных действия. С помощью функции ReadABitmapFromAFile изображение загружается из файла bridge.bmp в ресурс SplashBitmap. Затем обработчик устанавливает флаг SplashBitmapLoaded в TRUE, показывая, что нужно выводить заставку. И напоследок вызывается функция SetTimer, которая взводит таймер с идентификатором TIMER_ID на время DISPLAY_PERIOD. Поскольку эта функция, очевидно, не предназначена для рисования, то для получения контекста устройства DeviceContext вызывается функция GetDC. Контекст содержит все необходимое для того, чтобы ReadABitmapFromAFile мог- ла корректно прочитать пиксели из файла. После того как изображение загруже- но в ресурс, контекст возвращается системе с помощью функции ReleaseDC. СОВЕТ Обратите внимание, что здесь нет кода принудительной перерисовки, да он со- вершенно не нужен. Дело в том, что после обработки сообщения WM INITDIALOG Windows автоматически посылает программе сообщение WM_PAINT. Реализация обработчика DlgOnTimer По прошествии DISPLAY_PERIOD миллисекунд приложение получит сооб- щение WM_TIMER. Его обработчик должен подготовить переход от отображе- ния заставки к отображению нормального пользовательского интерфейса. void DlgOnTimer(HWND hDlg, UINT id) { if ( id == TIMER_ID ) { KillTimer(hDlg,TIMER_ID) ; DeleteObject(SplashBitmap) ; SplashBitmapLoaded = FALSE ; InvalidateRect(hDlg,NULL,TRUE) ; UpdateWindow(hDlg) ; } ) Получив это сообщение, обработчик вызывает функцию KillTimer, чтобы остановить таймер. Задача таймера - известить о том, что заставку пора убирать. Коль скоро эта цель достигнута, таймер больше не понадобится. Остановив таймер, программа вызывает функцию DeleteObject, чтобы унич- тожить ресурс SplashBitmap. Он занимает память в куче GDI, а эта куча имеет фиксированный размер и разделяется всеми приложениями Windows. Поэтому раз ресурс больше не нужен, занимаемую им память хорошо бы вернуть системе, что DeleteObject и делает.
176 Illi Обработка растровых изображений Затем обработчик сбрасывает флаг SplashBitmapLoaded в FALSE. Начиная с это- го момента, обработчик сообщения WM_PAINT будет обходить код вывода заставки. И в самом конце вызываются функции-InvalidateRect и UpdateWindow, кото- рые приводят к немедленной перерисовке окна. Заставка исчезает, вместо нее по- является обычный интерфейс. Модификация обработчика DlgOnPaint В обработчике DlgOnPaint флаг SplashBitmapLoaded определяет, рисовать ли заставку или обычный интерфейс. void DlgOnPaint(HWND hDlg) { // Строки, рассмотренные ранее, опущены if (SplashBitmapLoaded) { GetClientDimensions( hDlg, SClientWidth, &ClientHeight ) ; GetBitmapDimensions(SplashBitmap,&BitmapWidth, SBitmapHeight) ; DisplayABitmap(DeviceContext, SplashBitmap, 0, MENU_OFFSET, Clientwidth, ClientHeight, 0, 0, Bitmapwidth, BitmapHeight ) ; } else { OldTextColor = SetBkColor(DeviceContext,RGB(255,255, 255) ) ; ExtTextOut(DeviceContext,40,100,0,NULL, __TEXT("PocketPC Developer's Guide"),26,NULL) ; SetBkColor(DeviceContext,OldTextColor) ; } // Строки, рассмотренные ранее, опущены } Если переменная SplashBitmapLoaded равна TRUE, то выполняется код выво- да изображения, в противном случае вызывается функция ExtTextOut для вывода строки, из которой и состоит обычный интерфейс. Но перед вызовом ExtTextOut обработчик устанавливает цвет фона охватыва- ющего строку прямоугольника с помощью функции SetBkColor. СОВЕТ _______________________________________________ Если не задать цвет фона таким же, как для клиентской области, то текст будет отображаться на другом фоне, что неприятно поразит пользователя. После вывода текста в указанную позицию клиентской области обработчик возвращает контекст устройства DeviceContext в исходное состояние. Для этого снова вызывается SetBkColor, но на этот раз с тем цветом, который был сохранен в переменной OldTextColor при первом вызове. Анимация изображения Еще одно интересное и не вполне тривиальное применение функций из ком- понента BitmapUtilities — анимация растрового изображения. Анимип ' анное
Анимация изображения IIIIMM 177 изображение перемещается по клиентской области в ответ на сообщения WM_TIMER. Для демонстрации BitmapUtilities и функций Win32 API нужен еще один шаг. В этом примере одно изображение будет скользить по поверхности другого. Мы сможем полнее раскрыть возможности функции StretchBlt, лежащей в основе многих функций из компонента BitmapUtilities. Для перемещения изоб- ражения программа должна сначала поместить его в новое место, а затем быстро перерисовать освободившуюся область. Вообще-то такие действия влекут за со- бой выполнение большого числа операций над пикселями, а это занимает процес- сорное время. Однако StretchBlt работает на удивление быстро. Описание пользовательского интерфейса программы Пользовательский интерфейс этой программы, показанный на рис. 6.13, очень прост. Загружено фоновое и перемещающееся изображение. После загрузки запускается таймер Обработчик сообщения WM_TIMER перемещает маленькое изображение Imaqt* Animation Program Рис. 6.13. Пользовательский интерфейс программы анимации изображения В клиентской области присутствуют два изображения. Весь фон заполнен изображением кактуса. А поверх него расположено изображение безумного хаке- ра за компьютером. После того как оба изображения загружены в разные ресурсы, запускается таймер, который начинает посылать сообщения WM_TIMER При получении каждого сообщения изображение хакера перемещается в новое место. Для перемещения изображения программа должна решить ряд важных задач. Во-первых, нужно хранить клиентские координаты его левого верхнего угла и об- новлять их при получении сообщения WM_TIMER
178 1111 Обработка растровых изображений Кроме того, необходимо перерисовывать область, в которой изображение h.i ходилось в предыдущий момент. Если реализовать эту операцию плохо, то она будет потреблять много процессорного времени. И наконец, надо решить проблему мигания. Во время перерисовки часть ново- го изображения может оказаться в верхней части клиентской области, тогда как в нижней части еще видны остатки старого изображения. Такую ситуацию пользо- ватель воспринимает как мигание. Ясно, что это будет отвлекать и раздражать его. Все эти проблемы легко решаются за счет использования функций из компо- нента BitmapUtilities и прямого обращения к функции StretchBlt. Поскольку эта функция тщательно оптимизирована, то выполняется очень быстро. Реализация программы анимации изображения Для быстрого обновления картинки, состоящей из нескольких изображений, нужно предварительно подготовить ресурсы, играющие роль временного храни- лища. На рис. 6.14 показаны этот процесс и участвующие в нем ресурсы. Действия показаны сверху вниз. На первом шаге выполняется операция чте- ния, которая перемещает изображение из BMP-файла во внутренний ресурс. Фо- новое изображение хранится в ресурсе BGBitmap, а картинка на переднем плане - в ресурсе FGBitmap. <Bitmap> J ClientAreaBitmap Рис. 6.14. Процедура подготовки изображений для быстрого обновления Когда нужно вывести на экран комбинированное изображение, программа пере- ходит ко второму шагу. Результирующее изображение помещается в промежуточ-
Анимация изображения !11ИПМ 179 ный ресурс MirrorBitmap. Сначала программа копирует BGBitmap в MirrorBitmap, а затем поверх BGBitmap размещает FGBitmap, начиная с точки с заданными коорди- натами. При этом пиксели FGBitmap замещают пиксели BGBitmap. После того как ресурс MirrorBitmap подготовлен, достаточно просто вывес- ти его в нужную часть клиентской области. Эта часть обозначена на рис. 6.14 ClientAreaBitmap. Все необходимое для реализации этой программы находится в файле DlgProc.c. Тот факт, что никаких других файлов изменять не пришлось, демон- стрирует полезность повторно используемого компонента BitmapUtilities. Для анимации изображения в процедуру DlgProc нужно внести следующие изменения. 1. Объявить переменные для хранения исходных изображений. 2. Объявить переменную для хранения промежуточного изображения. 3. Объявить переменные для управления положением изображения на пе- реднем плане. 4. Объявить переменные для управления таймером. 5. Объявить локальные функции для работы с изображениями. 6. Изменить обработчики сообщений. 7. Реализовать локальные функции для работы с изображениями. Объявление переменных и констант Для управления процессом анимации изображения потребуется ряд перемен- ных и констант. Их можно разбить на четыре группы: для управления положе- нием изображения на переднем плане, для управления таймером, для хранения исходных изображений и для хранения промежуточного изображения. Ниже при- ведены все необходимые объявления. /*********************************************** * File: DlgProc.c ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ***********************************************/ // Объявление констант и переменных для управления положением // изображения на переднем плане ♦ define X_DELTA 5 ♦ define Y_DELTA 5 int CurrentXLocation ; int CurrentYLocation ; // Объявление констант для управления таймером ♦ define TIMER_ID 100 ♦ define DISPLAY_PERIOD 50 // Объявление переменных для управления исходными изображениями BOOL BrtmapsLoaded = FALSE ; HBITMAP BGBitmap ; HBITMAP FGBitmc
180 1111 Обработка растровых изображений // Объявление переменных для управления промежуточным изображением HDC MirrorDC ; HBITMAP MirrorBitmap ; HBITMAP OldBitmap ; Комментарии показывают, к какой группе относится та или иная переменная. В большинстве случаев имя переменной отражает ее назначение, но несколько элементов заслуживают более подробного обсуждения. Источником событий, заставляющих изображение на переднем плане переме- щаться, служит таймер. По прошествии каждых DISPLAY_PERIOD миллисе- кунд таймер генерирует сообщение WM_TIMER. В ответ изображение смещает- ся на X_DELTA пикселей по горизонтали и на Y_DELTA пикселей по вертикали. Эти смещения добавляются к величинам CurrentXLocation и CurrentYLocation, которые описывают позицию левого верхнего угла изображения в клиентских ко- ординатах. Двухэтапный процесс вывода комбинированного изображения требует нали- чия двух наборов ресурсов. В ходе инициализации изображение на переднем пла- не загружается в ресурс FGBitmap, а фоновое изображение - в ресурс BGBitmap. В ответ на сообщение WM_TIMER обработчик DlgOnTimer копирует оба этих изображения в промежуточный ресурс MirrorBitmap. Для создания такого изоб- ражения необходимо получить контекст устройства в памяти MirrorDC, к которо- му затем привязать изображение. Ресурс, находившийся в этом контексте раньше, сохраняется в переменной OldBitmap, чтобы потом его можно было восстановить. Реализация локальных вспомогательных функций В ответ на сообщения Windows выполняются некоторые важные операции. Вместо того чтобы включать их код непосредственно в обработчики сообщений, мы напишем несколько вспомогательных функций. void LoadTheBitmaps(HWND Window) { HDC DeviceContext ; int Bitmapwidth ; int BitmapHeight ; CurrentXLocation = 0 ; CurrentYLocation = MENU_OFFSET ; PreviousXLocation = CurrentXLocation ; PreviousYLocation = CurrentYLocation ; DeviceContext = GetDC(Window) ; ReadABitmapFromAFile(__TEXT("cactus.bmp"), DeviceContext , &BGBitmap ) ; ReadABitmapFromAFile(__TEXT("mad_hacker.bmp"), DeviceContext , SFGBitmap ) ; GetBitmapDimensions(BGBitmap,SBitmapWidth,SBitmapHeight) ; MirrorDC = CreateCompatibleDC(DeviceContext) ; MirrorBitmap = CreateCompatibleBitmap(DeviceContext, BitmapWidth, BitmapHeight) ; OldBitmap = SelectObject(MirrorDC,MirrorBitmap) ;
Анимация изображения НИИ 181 ReleaseDC(Window,DeviceContext) ; BitmapsLoaded = TRUE ; } Эта функция предназначена для инициализации различных ресурсов изобра- жений. От GetDC она получает контекст устройства для текущего окна. Имея описатель контекста, она вызывает функцию ReadABitmapFromAFile из компо- нента BitmapUtilities для загрузки изображений из файлов в ресурсы BGBitmap и FGBitmap. В заключение создается промежуточное изображение MirrorBitmap. Все описанные шаги нужно выполнять строго в указанной последовательности. void DisplayTheBitmaps(HWND Window, HDC DeviceContext) { int Clientwidth ; int ClientHeight ; int BGBitmapWidth ; int BGBitmapHeight ; int FGBitmapWidth ; int FGBitmapHeight ; GetClientDimensions(Window,SClientWidth, &ClientHeight) ; GetBitmapDimensions(BGBitmap,SBGBitmapWidth, sBGBitmapHeight) ; GetBitmapDimensions(FGBitmap,&FGBitmapWidth, &FGBitmapHeight) ; DisplayABitmap(MirrorDC, BGBitmap, 0, MENU_OFFSET, Clientwidth, ClientHeight, 0, 0, BGBitmapWidth, BGBitmapHeight ) ; DisplayABitmap(MirrorDC, FGBitmap, CurrentXLocation, CurrentYLocation, FGBitmapWidth, FGBitmapHeight, 0, 0, FGBitmapWidth, FGBitmapHeight ) ; StretchBlt(DeviceContext,0,0,BGBitmapWidth,BGBitmapHeight, MirrorDC,0,0,BGBitmapWidth, BGBitmapHeight,SRCCOPY) ; } Эта функция перемещает FGBitmap в новое положение на поверхности BGBitmap. Сначала BGBitmap с помощью функции DisplayABitmap копируется в ресурс MirrorBitmap, привязанный к контексту MirrorDC. Для DisplayABitmap безразлично, находится ли контекст в памяти или ассоциирован с буфером кадров и конкретным устройством отображения. Затем в MirrorBitmap тоже с помощью DisplayABitmap копируется изображение из ресурса FGBitmap. При этом указывает- ся начальная точка CurrentXLocation, CurrentYLocation. На этом первая фаза подго- товки к анимации заканчивается. Вторая фаза - обращение к функции StretchBlt для копирования MirrorBitmap из контекста MirrorDC в буфер кадров, с которым ассо- циирован контекст DeviceContext. В результате изображение FGBitmap оказывается в точке с координатами (CurrentXLocation, CurrentYLocation) в клиентской области, void updateTheBitmaps(HWND Window) { int Clientwidth ; int ClientHeight ; PreviousXI' "urrentXLocation ;
182 1111 Обработка растровых изображений PreviousYLocation = CurrentYLocation ; CurrentXLocation = CurrentXLocation + X_DELTA ; CurrentYLocation = CurrentYLocation + Y_DELTA ; GetClientDimensions(Window,SClientWidth, &ClientHeight) ; if ( CurrentXLocation > Clientwidth ) CurrentXLocation = 0 ; if ( CurrentYLocation > ClientHeight ) CurrentYLocation = MENU_OFFSET ; } Эта функция вычисляет новое положение изображения на переднем плане. К обеим координатам прибавляются смещения, а потом проверяется, находится ли новая точка в клиентской области. Для получения размеров клиентской облас- ти вызывается вспомогательная функция GetClientDimensions из компонента BitmapUtilities. Если хотя бы одна координата нового левого верхнего угла FGBitmap оказывается за пределами клиентской области, изображение возвра- щается в ее левый верхний угол. void ClearTheBitmaps(HWND Window) { Selectobject(MirrorDC,OldBitmap) ; ReleaseDC(Window,MirrorDC) ; DeleteObject(MirrorBitmap) ; DeleteObject(BGBitmap) ; DeleteObject(FGBitmap) ; } Эта функция возвращает различные объекты GDI системе, чтобы не занимать память в куче GDI, имеющей ограниченный размер. Функция SelectObject вос- станавливает исходное изображение в контексте MirrorDC. Больше этот контекст не понадобится, поэтому ReleaseDC возвращает его Windows. Все ресурсы, пред- ставляющие растровые изображения, - тоже объекты GDI. Занятая ими память освобождается путем обращения к DeleteObject. Модификация обработчиков сообщений Каждая из описанных выше вспомогательных функций вызывается из какого- то обработчика. В следующих листингах мы приводим только соответствующие фрагменты кода в том порядке, в котором обработчики вызываются во время ра- боты программы. BOOL OnlnitDialog ( HWND hDlg , HWND hDlgFocus , long UnitParam ) { // Ранее обсуждавшийся код опущен LoadTheBitmaps(hDlg) ; SetTimer(hDlg,TIMER_ID,DISPLAY_PERIOD,NULL) ; return TRUE ; } Непосредственно перед выходом обработчик OnlnitDialog вызывает функ- цию LoadTheBitmaps для загрузки изображений в ресурсы и подготег ги к анима-
Анимация изображения 183 ции. Затем с помощью SetTimer запускается таймер, который будет срабатывать каждые DISPLAY_PERIOD миллисекунд. void DlgOnTimer(HWND hDlg, UINT id) { if ( id == TIMER_ID ) { UpdateTheBitmaps(hDlg) ; InvalidateRect(hDlg,NULL,FALSE) ; UpdateWindow(hDlg) ; ) } Эта функция вызывается в ответ на сообщение от таймера. Убедившись, что ис- точником является именно таймер с идентификатором TIMER_ID, она обновляет положение FGBitmap, обращаясь к вспомогательной функции UpdateTheBitmaps. Следующая далее неразлучная пара InvalidateRect / UpdateWindow приводит к не- медленной перерисовке окна. Поскольку последним аргументом InvalidateRect является FALSE, то фон клиентской области не перерисовывается. Это была бы пустая трата времени, так как изображение BGBitmap занимает всю площадь кли- ентской области. К тому же будь этот флаг равен'TRUE, мы наблюдали бы мига- ние в момент между закрашиванием фона белым цветом и копированием поверх него изображения из ресурса MirrorBitmap. void DlgOnPaint(HWND hDlg) { // Ранее обсуждавшийся код опущен if (BitmapsLoaded) DisplayTheBitmaps(hDlg,DeviceContext) ; } Этот обработчик вызывается в результате обращения к InvalidateRect / UpdateWindow. Убедившись, что флаг BitmapsLoaded поднят и, значит, все изобра- жения подготовлены, он вызывает вспомогательную функцию DisplayTheBitmaps, которая и выполняет один шаг анимации. Флаг BitmapsLoaded необходим пото- му, что сообщение WM_PAINT впервые приходит еще до того, как обработчик сообщения WM_INITDIALOG загрузил изображения. Если бы мы попытались в этот момент вывести их на экран, программа завершилась бы аварийно. void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { switchf ilD ) { case IDOK: KillTimer(hDlg,TIMER_ID) ; ClearTheBitmaps(hDlg) ; EndDialog(hDlg , 0) ; break ; )
184 Обработка растровых изображений Для выхода из программы пользователь должен выбрать пункт меню Quit. При этом обработчик DlgOnCommand исполняет ветвь IDOK в предложении switch. Перед завершением диалога обработчик останавливает таймер (KillTimer) и с помощью вспомогательной функции ClearTheBitmaps возвращает системе все захваченные ресурсы. Подготовка ActiveSync для программ из этой главы Перед тем как запускать на Pocket PC программы, разработанные в этой гла- ве, необходимо перенести туда файлы с картинками. Для этого понадобится про- грамма Microsoft ActiveSync. Во время копирования ActiveSync по умолчанию преобразует все растровые изображения в формат 2 бита на пиксель. Чтобы этого не происходило, нужно изменить настройки ActiveSync. И дело не только в том, что написанные программы ожидают, что входные файлы имеют стандартный BMP-формат. Ведь если пиксель представлен двумя битами, то возможно всего четыре цвета, а при стандартных 24 битах на пиксель количество цве- тов многократно больше, значит, изображение выглядит намного детальнее. На рисунках ниже описаны шаги, которые нужно проделать, чтобы ActiveSync не преобразовывала растровые изображения при копировании. Сначала (рис. 6.15) нужно выбрать пункт меню Tools > Options. Если Pocket PC не подключен к настольному ПК, то все пункты меню будут неактивны, поэто- му предварительно нужно установить Pocket PC на подставку и подключить ее к порту компьютера. р Microsoft ActiveSync 1. Выбрать пункт меню Tools ЕНе—ViewfTools tfelp тф------Options... Sync 2. Выбрать пункт меню Options Guest Backup/Re store... Add/Remove Programs Connected 3. Устройство должно быть подключено Information Type j Status J Рис. 6.15. Задание параметров преобразования
Подготовка ActiveSync После выбора пункта меню Options открывается диалоговое окно, показанное на рис. 6.16. Наша задача - изменить параметры преобразования, поэтому нужно нажать кнопку Conversion Settings. В результате откроется окно File Conversion Pro- perties, показанное на рис. 6.17. В этом окне можно по отдельности настроить преобразования при копирова- нии с настольного ПК на Pocket PC и обратно. Настраивается способ преобразо- вания для файлов разных типов. Options s 1 i J Rules | http: //all - ebooks. com 1- £ S 1 i t [-Conflict Resolution If there is a conflict (an item has been chang' mobile device and desktop computer) ‘he Lrrr = tr₽'’onur’^sciv'sc (Click'Resch vA'c- tenitokeep) •i -vvs replace -be rte~i on mvjlev c ! j i x, s repfau* e en script "lie Conversion t-D Set howfiles will be converted when they a synchronised between this computer - tied, moved, or obile device Нажать кнопку Conversion Settings Cancel Рис. 6.16. Выбор параметров преобразования Мы хотим изменить способ преобразования при копировании растровых изображений с настольного ПК на Pocket PC. Поэтому нужно: 1) перейти на вкладку Desktop to Device; 2) выбрать тип файлов Bitmap Image; 3) нажать кнопку Edit. В результате откроется следующее диалоговое окно Edit Conversion Settings (рис. 6.18).
186 Обработка растровых изображений | File Conversion properties • General | Device to Desktop Desktop to Device -|i--------------- 3 You can adjust conversion settings forftles moved from the desktop | computer to your mobile device Desktop computer convertible file types jDumap ;ma<y| ©Microsoft Word Document ©Microsoft Word Template S Font file ©Microsoft Access Application ©Microsoft PowerPoint Presentation | Pocket Word Document Г nswl 1. Перейти на вкладку Desktop to Device 2. Выбрать тип файлов Bitmap Image 3. Нажать кнопку Edit EL a conversion details —— esktop computer files of the tvpe JL Sitmap Image onver jbile device files . Srtmap Image OK Cancel > - р Help Рис. 6.17. Выбор файлов, содержащих растровые изображения • Edit Conversion Settings: Bitmap Image ir' e When converting from desktop computer files ofthe type - Bitmap Image bmp 4 Convert to mobile device files ofthe type — Type i|(No conversion) Г-bmp) • 1. Выбрать из раскрывающего списка вариант No Conversion J Тransfer fils without converting it bmp OK | Cancel 2. Нажать кнопку OK Рис. 6.18. Отмена преобразования растровых изображений
Резюме IIIIIBH 187 Вид преобразования выбирается из раскрывающего списка Туре. Выберите вариант No Conversion (Без преобразования). При такой настройке ActiveSync не станет преобразовывать растровое изображение в формат 2 бита на пиксель, и все приведенные в этой главе программы будут работать правильно. Резюме В этой главе мы показали, как обогатить пользовательский интерфейс за счет использования полноценных растровых изображений. Поскольку при работе с изображениями часто выполняются повторяющиеся действия, мы инкапсулиро- вали в компоненте BitmapUtilities ряд вспомогательных функций. С их помощью программу для Pocket PC можно записать в виде последовательности логических операций над изображениями, а не дублировать цепочки низкоуровневых вызо- вов Win32 API. Компонент BitmapUtilities применен в трех разных программах. □ Во всех операциях с растровыми изображениями, включая и вошедшие в состав компонента BitmapUtilities, действия производятся над ресурсом Windows, представляющим изображение. Такой объект предоставляет GDI. □ Как правило, при выполнении программы жизненный цикл любой слож- ной структуры данных описывается цепочкой «внешняя форма - внутрен- няя форма - локальная память - внутренняя форма - внешняя форма». Эта цепочка может стать объектом автономного тестирования. □ В алгоритме обнаружения краев к изображению применяется ядро фильт- ра, то есть каждый пиксель заменяется взвешенной суммой соседних пик- селей. □ При выводе изображения на экран Pocket PC программа явно корректиру- ет положение левого верхнего угла изображения, иначе верхняя часть ока- залась бы перекрытой полосой меню. □ Для анимации изображения необходим промежуточный ресурс в памяти, в котором строится комбинация изображений переднего и заднего плана. Затем построенное изображение выводится в клиентскую область окна. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Программа обработки изображений для настольного ПК Программа обработки изображений для Pocket PC Программа вывода заставки для настольного ПК Программа вывода заставки для Pocket PC Программа анимации изображения для настольного ПК Программа внимании изображения для Pocket PC Папка ImageProcessingProgram ImageProcessingProgramPPC SplashScreenProgram SplashScreenProgramPPC ImageAnimationProgram ImageAnimationProgramPPC
188 HBIIH Обработка растровых изображений Инструкции по сборке и запуску Программа обработки изображений для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект ImageProcessingProgram.dsw в папке ImageProcessing- Program. 3. Соберите программу. 4. Запустите программу. 5. Выберите пункт меню Select > Input. 6. В диалоговом окне File Open выберите файл hardware.bmp и нажмите кнопку Open. 7. Выберите пункт меню Image > Load. 8. Выберите пункт меню Image > Display. В клиентской области появится изображение инструментов. 9. Выберите пункт меню Image > Filter. В клиентской области появится изоб- ражение контуров отдельных элементов. Контуры будут представлены бе- лыми пикселями, все остальное - черными. 10. Выберите пункт меню Select > Output. И. В поле File Name в диалоговом окне File Save введите имя файла edges.bmp и нажмите кнопку Save. 12. Выберите пункт меню Image > Dump. 13. Выберите пункт меню Image > Store. 14. Выберите пункт меню Quit. 15. Окно закроется, так как приложение завершило работу. 16. Выберите файл edges.bmp, загрузите его и выведите на экран, дабы убе- диться в том, что изображение с выделенными краями было успешно со- хранено. Программа обработки изображений для Pocket PC 1. Подготовьте ActiveSync к копированию растровых изображений на КПК без преобразования. 2. Подключите подставку КПК к настольному компьютеру. 3. Поставьте КПК на подставку. 4. Попросите программу ActiveSync создать гостевое соединение. 5. Убедитесь, что соединение установлено. 6. С помощью программы Windows Explorer скопируйте файл BLDG.BMP с настольного ПК на КПК. Если все было сконфигурировано правильно, то ActiveSync не станет преобразовывать этот ВМР-файл. 7. Запустите Embedded Visual C++ 3.0. 8. Откройте проект ImageProcessingProgramPPC. vcw в папке ImageProcessing- ProgramPPC. 9. Соберите программу. 10. Убедитесь, что программа успешно загрузилась в КПК. 11. На КПК запустите File Explorer.
Примеры программ в Web 189 12. Перейдите в папку MyDevice. 11. Запустите программу ImageProcessingProgram. 13. Выберите пункт меню Select > Input. 14. В диалоговом окне File Open выберите файл hardware.bmp и нажмите кнопку Open. 15. Выберите пункт меню Image > Load. 16. Выберите пункт меню Image > Display. В клиентской области появится изображение инструментов. 17. Выберите пункт меню Image > Filter. В клиентской области появится изображение контуров отдельных элементов. Контуры будут представле- ны белыми пикселями, все остальное - черными. 18. Выберите пункт меню Select > Output. 19. В поле File Name в диалоговом окне File Save введите имя файла edges.bmp и нажмите кнопку Save. 20. Выберите пункт меню Image > Dump. 21. Выберите пункт меню Image > Store. 22. Выберите пункт меню Quit. 23. Окно закроется, так как приложение завершило работу. 24. Выберите файл edges.bmp, загрузите его и выведите на экран, дабы убе- диться в том, что изображение с выделенными краями было успешно со- хранено. Программа вывода заставки для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект SplashScreenProgram.dsw в папке SplashScreenProgram. 3. Соберите программу. 4. Запустите программу. 5. Сначала программа выводит в клиентскую область изображение моста. 6. Через некоторое время картинка исчезает, а вместо нее появляется белая клиентская область, в середине которой выведена строка «Pocket PC Developer’s Guide». 7. Выберите пункт меню Quit. 8. Окно закроется, так как приложение завершило работу. Программа вывода заставки для Pocket PC 1. Подготовьте ActiveSync к копированию растровых изображений на КПК без преобразования. 2. Подключите подставку КПК к настольному компьютеру. 3. Поставьте КПК на подставку. 4. Попросите программу ActiveSync создать гостевое соединение. 5. Убедитесь, что соединение установлено. 6. С помощью программы Windows Explorer скопируйте файл BRIDGE.BMP с настольного ПК на КПК. Если все было сконфигурировано правильно, то ActiveSync не станет преобразовывать этот ВМР-файл.
190 1111 Обработка растровых изображений 7. Запустите Embedded Visual C++ 3.0. 8. Откройте проект SplashScreenProgramPPC.vcw в папке SplashScreen- ProgramPPC. 9. Соберите программу. 10. Убедитесь, что программа успешно загрузилась в КПК. 11. На КП К запустите File Explorer. 12. Перейдите в папку MyDevice. 13. Запустите программу SplashScreenProgram. 14. Сначала программа выводит в клиентскую область изображение моста. 15. Через некоторое время картинка исчезает, а вместо нее появляется белая клиентская область, в середине которой выведена строка «Pocket PC Developer’s Guide». 16. Выберите пункт меню Quit. 17. Окно закроется, так как приложение завершило работу. Программа анимации изображения для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект ImageAnimationProgram.dsw в папке ImageAnimation- Program. 3. Соберите программу. 4. Запустите программу. 5. Сначала программа выводит фотографию кактуса и изображение безумно- го хакера в левом верхнем углу. 6. По прошествии некоторого времени картинка с хакером перемещается на другое место. Освободившаяся область мгновенно перерисовывается, так что пользователь не замечает мигания. 7. Картинка с хакером быстро и плавно перемещается по клиентской области. 8. Выберите пункт меню Quit. 9. Окно закроется, так как приложение завершило работу. Программа анимации изображения для Pocket PC 1. Подготовьте ActiveSync к копированию растровых изображений на КПК без преобразования. 2. Подключите подставку КПК к настольному компьютеру. 3. Поставьте КПК на подставку. 4. Попросите программу ActiveSync создать гостевое соединение. 5. Убедитесь, что соединение установлено. 6. С помощью программы Windows Explorer скопируйте файлы CACTUS.BMP и MAD HACKER.BMP с настольного ПК на КПК. Если все было сконфи- гурировано правильно, то ActiveSync не станет преобразовывать эти ВМР- файлы. 7. Запустите Embedded Visual C++ 3.0. 8. Откройте проект ImageAnimationProgramPPC.vcw в папке ImageAnimation- ProgramPPC.
Примеры программ в Web lllinni 191 9. Соберите программу. 10. Убедитесь, что программа успешно загрузилась в КПК. И. На КПК запустите File Explorer. 12. Перейдите в папку MyDevice. 13. Запустите программу ImageAnimationProgram. 14. Сначала программа выводит фотографию кактуса и изображение безум- ного хакера в левом верхнем углу. 15. По прошествии некоторого времени картинка с хакером перемещается на другое место. Освободившаяся область мгновенно перерисовывается, так что пользователь не замечает мигания. 16. Картинка с хакером быстро и плавно перемещается по клиентской области. 17. Выберите пункт меню Quit. 18. Окно закроется, так как приложение завершило работу.
http: //all - ebooks. com Глава 7. Проектирование эффективных программ С помощью программы, разработанной в главе 5, можно рисовать только прямые линии. Надо признать, что полезность такой программы несколько ограничена. В этой главе мы обобщим ее на другие фигуры: прямоугольник, скругленный прямо- угольник и эллипс. При этом ее придется подвергнуть полной переработке. С учетом высказанных критических замечаний в новом варианте будет использоваться единст- венная инкапсулированная переменная состояния, управляющая конечным авто- матом и таблицей действий. Кроме того, мы поместим управление пользовательским интерфейсом, управление логикой работы и управление данными в разные уровни. Простейшее средство выбора из набора альтернатив - меню, содержащее пе- речень заранее заданных вариантов. Если пользователь выберет из него какой-то стиль рисования, то программа запомнит его и будет применять к последующим операциям. Для эффективного управления данными выбранный стиль будет хра- ниться в инкапсулирующем компоненте. Поскольку он предоставляет всем своим клиентам значения по умолчанию, назовем его DefaultMgr. Программа может находиться в одном из четырех состояний: бездействие, ри- сование, подготовка к вводу и ввод. Вместо того чтобы использовать для их пред- ставления четыре разные переменные, мы заведем одну переменную состояния. В сочетании с конечным автоматом она позволит сосредоточить всю логику приня- тия решений в одной функции. Поэтому отладка программы намного упростится, что повысит продуктивность программиста и сократит время выхода на рынок. Реализация программы рисования в главе 5 оказалась сложной и запутанной. И это увеличивает время добавления в программу новых возможностей. В этой главе мы полностью перепроектируем ее, вынеся управление интерфейсом, логи- ку принятия решений и управление данными на разные уровни. Каждый уровень обращается за помощью к функциям, находящимся на уровень ниже. Таким обра- зом, при добавлении новой функциональности можно воспользоваться средства- ми, реализованными на нижних уровнях, и работа программиста упрощается. ПРИМЕЧАНИЕ________________________________________________ Вопросам проектирования на ранних стадиях разработки следует уделять боль- ше внимания, чем кодированию. Чтобы создать продукт, уложившись в отведен- ное время, разработчик должен примерно половину своего времени посвятить изучению требований и проектированию - еще до написания первой строчки кода. Но большинство программистов начинают кодировать уже с первого для, надеясь заставить программу работать на этапе интеграции.
Обоснование выбранного подхода Ш1ВШ 193 Обоснование выбранного подхода к проектированию Наша цель - предложить эффективный подход к реализации программы ри- сования. Проект должен обеспечить расширяемость, то есть простоту добавления новых возможностей, а также продуктивность программиста и производитель- ность. В этой главе мы применим эволюционный подход к проектированию. На примере разных частей программы мы продемонстрируем отдельные элементы проекта, и каждый новый элемент будет вносить вклад в эффективность проекта в целом. В итоге получится многоуровневая программа, в которой интерфейс пользователя, логика управления и управление данными размещены на разных ПРИМЕЧАНИЕ Расширяемый проект - это результат выделения отдельных элементов програм- мы и применения техники абстрагирования данных. Модификации должны ка- саться только одного уровня. Как правило, внесение изменений в структуру и логику одного уровня не должно распространяться на другие. Инкапсуляция на нижних уровнях также повышает продуктивность, поскольку элементы, располо- женные выше, могут опираться на уже реализованную функциональность. Интерфейс пользователя представлен на рис. 7.1. уровнях. 4. Буксировать мышь с нажатой левой кнопкой 5. Отпустить левую кнопку мыши Рис. 7.1. Управляемый меню интерфейс программы рисования Помимо пункта Quit, в меню появился еще пункт Shape. Цифрами на рис. 7.1 обозначена последовательность операций, выполняемых пользователем.
194 ШИ Проектирование эффективных программ 1. Выбрать из меню Shape фигуру: прямую линию, прямоугольник, скруглен- ный прямоугольник или эллипс. 2. Указать левой кнопкой мыши на любую точку в клиентской области. На- жать и удерживать левую кнопку мыши. 3. Отбуксировать мышь в другую точку. При этом программа рисует охваты- вающий прямоугольник, противоположные углы которого располагаются в начальной точке и той, где сейчас находится курсор. 4. Отпустить левую кнопку мыши. Выбранная фигура вписывается в охваты- вающий прямоугольник и остается на экране. Здесь используются операции рисования эластичного контура, реализован- ные в главе 5. Однако теперь они применяются для рисования не только отрезка, но и других фигур. Работа с меню описывается специальными терминами, смысл которых пояс- няется на рис. 7.2. Полоса главного — меню, состоящего из пунктов Mt3nu Usaqe Pvuqram Ниспадающее подменю Ellipse Line „ - Rectangle Round Rectangle < Отмеченный пункт подменю Выделенный пункт подменю Рис. 7.2. Компоненты меню В верхней части окна расположена полоса главного меню, закрашенная серым цветом. В ней находится одна или несколько меток. Каждая метка называется пунктом меню. Обычно когда пользователь щелкает по пункту главного меню, появляется ниспадающее подменю, также состоящее из пунктов. В подменю могут быть ниспадающие подменю второго уровня. В некоторых программах встречаются подменю и более высокого уровня вложенности. Глуби- на вложенности не ограничена. Когда пользователь проводит мышью по главному меню, пункт, находящийся под курсором, обводится рамкой - выделяется. Выделение служит визуальным указанием на то, какой пункт будет выбран при нажатии кнопки мыши. Пункты
Обоснование выбранного подхода 1111ПН 195 следнии раз. подменю тоже выделяются, но по-другому: текущий пункт рисуется другим цве- том (обычно синим). Цель выделения - дать пользователю возможность подумать, прежде чем сде- лать выбор. Другой полезный визуальный признак информирует пользователя о том, что он выбрал в меню раньше. Речь идет о помеченных пунктах подменю. Если, напри- мер, меню служит для выбора стиля линии, то пометка показывает, какой стиль сейчас является текущим. Поскольку в меню может быть десяток альтернатив, то было бы неправильно заставлять пользователя помнить, какую он выбрал в по- ПРИМЕЧАНИЕ Использование меню - крайне неудачный способ организации графического ин- терфейса пользователя, особенно в случае Pocket PC. Все виды фигур можно было разместить и в главном меню. Но для Pocket PC это было бы плохим проектным решением. По мере включения в программу но- вых возможностей главное меню становилось бы все более громоздким, и отде- лить один набор альтернатив от другого стало бы затруднительно. Меню очень неудобно для управления последовательностью действий пользователя. В большинстве приложений пользователь обычно должен выпол- нять действия в определенном порядке. Например, сначала установить связь с ап- паратурой, потом открыть файл параметров. И все, что пользователю нужно, должно присутствовать в текущем окне. Меню, перекрывающее главное окно, мо- жет скрыть ту информацию, которая необходима для принятия решения. В главе 8 мы покажем куда более удачный способ организации сложных пользовательских интерфейсов, основанный на вкладках. Учитывая малую пло- щадь экрана Pocket PC, такой интерфейс оказывается более дружелюбным. Меню и подменю - это средства взаимодействия пользователя с программой. Пользователь выбирает какой-то пункт меню, а программа выполняет соответст- вующую операцию. На рис. 7.3 показано, как программа реагирует на выбор из меню. 1. Пользователь касается стилосом пункта подменю. 2. Windows генерирует сообщение WMCOMMAND, помещая в него уни- кальный идентификатор выбранного пункта. Это сообщение поступает об- работчику DlgOnCommand в диалоговой процедуре DlgProc. 3. Обработчик реагирует на выбор пункта меню. В данном случае выбранный вид геометрической фигуры передается в структуру данных DefaultValues. 4. Когда пользователь касается пункта Shape в главном меню, появляется ниспадающее подменю. Но перед тем как вывести его на экран, Windows генерирует сообщение WM_INITMENUPOPUP. Обработчик этого сооб- щения устанавливает галочку против текущего вида фигуры. Тем самым пользователю передается информация о том, какой пункт подменю он вы- бирал раньше
196 HIMBHIIII Проектирование эффективных программ 5. Для того чтобы узнать, какой пункт меню нужно пометить галочкой, обра- ботчик обращается к структуре DefaultValues. 6. Против ранее выбранного пункта подменю ставится галочка, которую пользователь увидит. 1- 6. Пользователь щелкает Текущий пункт меню по пункту меню отмечен 2. 4.______ (WM COMMAND]t--------------------------------Wwmjnitmenupopur] Сохраненные данные /к Обновить с использованием 3- / 5- сохраненных данных * Defaultvaluesх Выбираемое по умолчанию значение обновлено Рис. 7.3. Обработка сообщений, связанных с выбором из меню Программе не нужно самой подсвечивать пункт подменю, находящийся под курсором мыши, этим занимается компонент Windows USER. Обратите внимание на структуру данных DefaultValues. В ней, в частности, хранится вид выбранной в последний раз геометрической фигуры. Применение компонента DefaultMgr для поддержки работы с меню показано на рис. 7.4. Компонент DefaultMgr хранит глобальные переменные, доступные прочим час- тям программы. Так, в ответ на выбор пункта подменю обработчик DlgOnCommand вызывает функцию SetDefaultShape для запоминания выбранной фигуры. ПРИМЕЧАНИЕ Поскольку компонент DefaultMgr хранит единственный экземпляр каждой из пе- ременных, описывающих текущий стиль, его можно назвать менеджером объек- тов. Менеджер объектов предоставляет такой вид инкапсуляции, при котором запоминаются одиночные значения. Другие части программы обращаются к текущим значениям по умолчанию с помощью функции Get. Если говорить об обработке пунктов меню, то функция DlgOnlnitMenuPopup перед выводом подменю на экран получает текущий вид фигуры для пометки соответствующего пункта. Для этого вызывается функция GetDialogShape. Показанная на рис. 7.4 структура DefaultValues полностью скрыта от осталь- ной части программы, но видна компоненту DefaultMgr. Никаким другим спосо- бом, кроме вызова функций Get и Set, добраться до нее невозможно. Такое сокры- тие информации возможно вследствие применения статических переменных внутри компонента DefaultMgr.
Обоснование выбранного подхода НИШ 197 Рис. 7.4. Задание текущих значений по умолчанию с помощью меню Выбрав вид фигуры, пользователь, вероятно, захочет ее нарисовать. При этом надо будет обрабатывать сообщения WM_MOUSEMOVE, как то было показано в главе 5. Но теперь программа будет обращаться к необходимым для рисования данным через инкапсулирующий их менеджер. На рис. 7.5 показано сочетание объектов рисования и компонента DefaultMgr во время обработки сообщений WM_MOUSEMOVE. Как видите, нужны два объекта рисования. В переменной PreviousDrawObject хранятся вид и конечные точки последнего нарисованного объекта, а в перемен- ной CurrentDrawObject - вид и конечные точки объекта, который только предсто- ит нарисовать. Чтобы поместить вид фигуры в CurrentDrawObject, программа запрашивает текущее значение по умолчанию у DefaultMgr. Цифрами на рисунке обозначены последовательные шаги, а обведенная кружочком буква показывает, как данные, управляемые DefaultMgr, перемещаются в объекты рисования PreviousDrawObject и CurrentDrawObject. В программе из главы 5 процесс рисования описывался четырьмя переменны- ми: DragStart, DragStop, CurrentX и CurrentY. В новой реализации нужно только два объекта: PreviousDrawObject и CurrentDrawObject. Код, в котором использу- ются объекты рисования, оказывается проще и понятнее, чем при наличии четы- рех переменных.
198 Illi Проектирование эффективных программ 1. Буксируется мышь 8. В окне появляется изображение 2.(WM MOUSEMOVE]_ 3. Сохраненные данные 5. InvalidateRect, UpdateWindow 4. PreviousDrawObject CurrentDrawObject ^(wm paint) 7. Рисовать с использованием сохраненных данных Defaultvalues—Ка) Объекты рисования запомнены в буфере Рис. 7.5. Использование объектов рисования и значений по умолчанию ПРИМЕЧАНИЕ Удачно спроектированная программа оказывается проще, у разработчика уходит меньше времени на сборку ее из частей и тестирование, поэтому время выхода на рынок сокращается. Менеджер счастлив, а программист может немного от- дохнуть и расслабиться. Ниже приведено линейное описание последовательности работы программы, представленной на рис. 7.5. 1. Пользователь буксирует мышь. 2. Диалоговой процедуре поступает сообщение WMMOUSEMOVE. 3. Основная задача обработчика этого сообщения - запомнить необходимые данные в объектах рисования. 4. Обработчик копирует CurrentDrawObject в PreviousDrawObject. После этого состояние CurrentDrawObject можно изменить. Новые значения за- прашиваются в DefaultMgr. 5. Далее обработчик вызывает функции InvalidateRectangle и UpdateWindow для перерисовки клиентской области. 6. Программа получает сообщение WM_PAINT и вызывает обработчик DlgOnPaint. 7. Этот обработчик получает свойства объекта рисования от DefaultMgr и, пользуясь ими, стирает старый объект и рисует новый. 8. Пользователь видит, как исчезает старый объект рисования и появляется новый, что создает эффект эластичного контура. В этой последовательности отдельные переменные заменены объектами рисо- вания. По существу, объект рисования - автономная сущность, в которой хранят- ся все данные, определяющие текущую операцию рисования. Упаковка их в еди-
Обоснование выбранного подхода 199 ный, легко управляемый контейнер позволяет без труда модифицировать и рас- ширять возможности рисования. ПРИМЕЧАНИЕ Для поддержки нескольких экземпляров объектов рисования понадобится другой механизм инкапсуляции, а именно менеджер типов данных, или просто менед- жер типов. Первым аргументом каждой функции, принадлежащей менеджеру ти- пов DrawObjMgr, является объект типа DrawObjectType. На рис. 7.6 представлен новый менеджер типов DrawObjMgr. Здесь же мы ви- дим, как в программе рисования хранятся и используются конкретные экземпля- ры DrawObjectType. DlgProc DlgOnMouseMove DlgOnPaint | Фигура DefaultMgr |<DrawObject> |<Shape> <Brush> < DeviceContext> J <DrawObject> £ ♦ GetDefaultShape <DefaultValues> DrawObjMgr i* PutDatalntoShape DrawShape <Rectangle> <Shape> <LineWidth> DataMgr <CurrentDO> <LineStyle> <LineColor> <FIIIBrush> A —------------► GetCurrentDO <PreviousDO> —i-------------- • p-""1 —--------------► GetPreviousDO <CurrentDO> <PreviousDO> Рис. 7.6. Применение объектов рисования для изображения эластичного контура
200 1111 Проектирование эффективных программ Как видите, компонент DrawObjMgr управляет набором свойств конкретного экземпляра DrawObjectType, например Shape, Rectangle, Line Width и др. Все по- казанные на рисунке функции ожидают, что первым аргументом будет передан DrawObject. Маленькие стрелки с кружочком в основании указывают на функ- цию и обозначают, что ей передается DrawObject. СОВЕТ На рис. 7.6 показаны лишь те методы, поддерживаемые инкапсулированными компонентами, на которые есть ссылки в тексте главы. На рисунке мы видим три важных инкапсулированных компонента. Компо- нент DrawObjMgr - это менеджер типов для экземпляров DrawObjectType. Выше уже рассматривался другой компонент - DefaultMgr. Он играет роль менеджера объектов для одиночных значений, описывающих стиль рисования, выбранный из меню. В левом нижнем углу рисунка показан еще один компонент - DataMgr. Он использовался в программе, разработанной в главе 3, а сейчас служит менед- жером объектов для двух важных экземпляров DrawObjectType: PreviousDraw- Object и CurrentDrawObject. ПРИМЕЧАНИЕ Три компонента - DrawObjMgr, DefaultMgr и DataMgr - образуют уровень управ- ления данными, самый нижний в программе. Их общее назначение - отделить детали хранения данных от остальной программы. Показанные на рисунке взаимодействия и взаимосвязи компонентов прояс- няют порядок работы программы. 1. Когда пользователь буксирует мышь, диалоговой процедуре поступают со- общения WM_MOUSEMOVE. 2. Эти сообщения передаются обработчику DlgOnMouseMove. 3. Обработчик запрашивает у компонента DataMgr экземпляры Previous- DrawObject, CurrentDrawObject и MouseLocation, пользуясь функциями GetPreviousDrawObject, GetCurrentDrawObject и GetMouseLocation соот- ветственно (на рисунке они не показаны). 4. Затем обработчик вызывает функцию GetShapeBoundingRect (не показа- на), предоставляемую компонентом DrawObjMgr, чтобы получить прямо- угольник, охватывающий CurrentDrawObject. Потом у DefaultMgr он за- прашивает текущие атрибуты рисования, например Shape, LineWidth, и т. д. 5. Далее обработчик вызывает функцию PutDatalntoShape, чтобы сохранить охватывающий прямоугольник и атрибуты в объекте PreviousDrawObject. 6. Наконец, с помощью метода PutDatalntoShape обработчик заносит в объект CurrentDrawObject левый верхний угол охватывающего прямоугольника, координаты мыши и атрибуты рисования.
Обоснование выбранного подхода 11Н1ПН 201 7. Установив свойства объектов рисования, обработчик последовательно вызывает функции InvalidateRect и UpdateWindow, чтобы перерисовать клиентскую область. 8. При этом вызывается обработчик DlgOnPaint сообщения WM_PAINT. 9. Этот обработчик получает от компонента DataMgr объекты рисования с помощью функций GetPreviousDrawObject и GetCurrentDrawObject. (Для представления данного взаимодействия служат обведенные кружоч- ком буквы-соединители. Полностью нарисованные линии загромоздили бы диаграмму.) 10. Затем обработчик устанавливает бинарную растровую операцию и переда- ет контекст устройства и оба объекта рисования компоненту DrawObjMgr, который и рисует эластичный контур. Проследите взаимодействия, представленные на рис. 7.6. Они четко показыва- ют, как инкапсуляция данных внутри менеджеров объектов (DataMgr и Default- Mgr) и менеджера типов (DrawObjMgr) обеспечивает расширяемость программы. К сожалению, одного лишь добавления этих компонентов еще недостаточно. В текущей реализации по-прежнему имеется целый ряд переменных состояния, логика управления которыми довольно запутана. Интерфейс пользователя может находиться в одном из нескольких состояний, а переходы из одного в другое про- изводятся внутри обработчиков. Поэтому следующим шагом мы введем в про- грамму конечный автомат, с помощью которого будем управлять состоянием ин- терфейса и выбирать реакцию на действия пользователя. Конечный автомат для программы рисования состоит из четырех состояний: бездействие, рисование, подготовка к вводу и ввод. Состояния и переходы между ними изображены на рис. 7.7. Здесь прямоугольниками представлены состояния программы. Стрелки обо- значают допустимые переходы между состояниями. Каждая стрелка исходит из предыдущего состояния и ведет в следующее. Переходы помечены надписями. Над чертой написано имя «события», выз- вавшего переход. Это может быть название какого-то сообщения Windows, воз- можно, в сочетании с именем параметра сообщения. Под чертой находится ин- формация о действии, предпринимаемом в ответ на сообщение. ПРИМЕЧАНИЕ Состояния программы описывают историю действий пользователя или последо- вательность действий, необходимых для создания некоторых условий. Согласно рис. 7.7, программа рисования может находиться в одном из следую- щих состояний. □ Бездействие. Программа ожидает действия пользователя. □ Рисование. Выполняется рисование эластичного контура. □ Подготовка к вводу. Пользователь хочет вводить текст, но еще не указал начальную точку. Q Ввод. Нача. >чка текста аалана и плпкалкатрпк ййпгшт симвлпы
202 1111 Проектирование эффективных программ [InitlnputProcessing] Бездействие <LButtonDown> [StartDrawing] CKeyDown, BackSpace> [StartTyping] Рисование <MouseMove> [ProcessMouse] <Command, IDOK> [TermlnputProcessing] Подготовка к вводу <LButtonDown> [PositionTestString] <LButtonUp> [StopDrawing] CKeyDown, Character> [Processcharacter] CKeyDown, Backspace> [StopTyping] Рис. 7.7. Конечный автомат для управления взаимодействием с пользователем Переходы между состояниями «подготовка к вводу» и «ввод» описывают дейст- вия, которые должен предпринять пользователь, прежде чем сможет приступить к вводу текста. Рассмотрим, к примеру, переход из состояния «бездействие» в состояние «ри- сование». Метка рядом с этим переходом показывает, что он происходит, если в со- стоянии «бездействие» программа получает сообщение WM_LBUTTONDOWN. В ответ она выполняет действие, обозначенное StartDrawing, после чего перехо- дит в состояние «рисование». Наш конечный автомат отличается от тех, что приведены во многих других книгах. Обычно в каждом состоянии имеются возвратные переходы. Они описы- вают события, которые хотя и приводят к выполнению некоторых действий, но оставляют автомат в прежнем состоянии. Таких событий может быть несколько. Поскольку обрабатывать их все же необходимо, для каждого такого события не- обходима запись в таблице переходов. Очевидный пример такой ситуации - сообщение WM_MOUSEMOVE, посту- пающее, когда программа находится в состоянии «рисование». Согласно рис. 7.7, это сообщение вызывает действие ProcessMouse. Ясно, что без этого действия на- рисовать эластичный контур было бы невозможно. ПРИМЕЧАНИЕ ................ Если не включить важные возвратные переходы в сложный конечный автомат, то проблемы при окончательной сборке и тестировании программы гарантированы, а это сильно затянет сроки разработки.
Обоснование выбранного подхода Н111НШМ 203 Обратите внимание, как конечный автомат позволяет по-разному обрабаты- вать сообщение WM_LBUTTONDOWN в разных контекстах. Если это сообще- ние поступает в состоянии «бездействия», то выполняется действие StartDrawing. Если же программа находилась в состоянии «подготовка к вводу», то выполняет- ся действие PositionTextString. СОВЕТ Зависимость реакции от текущего состояния программы не оставляет места нео- пределенности и показывает, как именно конечный автомат «сохраняет воспоми- нания» о предыдущих взаимодействиях. Следуя по стрелкам переходов состояний, легко проследить последователь- ность выполняемых операций. Например, в таблице ниже приведена линейная последовательность операций рисования. Предыдущее состояние Событие Действие Новое состояние Бездействие Рисование (•••) Рисование Рисование WM_LBUTTONDOWN WM_MOUSEMOVE StartDrawing ProcessMouse Рисование «Рисование WM_MOUSEMOVE WM_LBUTTONUP ProcessMouse StopDrawing Рисование Бездействие Многоточие в средней строке представляет серию сообщений WM_MOUSEMOVE. Они поступают, пока пользователь буксирует мышь с на- жатой левой кнопкой. При внимательном изучении этого конечного автомата выявляется одно упу- щение. Находясь в состоянии «ввод», пользователь может сразу перейти к рисо- ванию, щелкнув левой кнопкой мыши. Необходимо распознавать этот переход и реагировать на него. Обратите, однако, внимание на то, что этот изъян можно най- ти, не имея ничего другого, кроме самого конечного автомата. Подобные автома- ты позволяют не только отыскивать логические ошибки, но и легко добавлять но- вые правила принятия решений. Добавьте в диаграмму новый переход, пометьте его - и в программе образуется дополнительная логическая ветвь. ПРИМЕЧАНИЕ Применение конечных автоматов позволяет выявить логические ошибки в проек- те и одновременно расширить проект для исправления этих ошибок. Хотя конечный автомат необходим для описания логики принятия решений в программе, его недостаточно для полного определения реализации. Напомним, что с каждым переходом ассоциировано имя действия. Теперь предстоит опреде- лить семантику каждого действия. Для этой цели применяется таблица действий, в которой прописаны детали обработки.
204 III! Проектирование эффективных программ Таблица действий, соответствующая приведенному выше конечному автома- ту, приведена в табл. 7.1. В каждой строке описано одно действие. В первой колонке мы видим то же имя, что было указано на диаграмме, а во второй - функции, которые нужно вы- зывать для реализации действия. Сколько бы раз некоторое действие ни встреча- лось в диаграмме, в таблице под него отведена единственная строка. Таблица 7.1. Таблица, описывающая обработку действий пользователя Имя действия Список функций [InitlnputProcessing] InitializeDrawingData, InitializeTextData, SetDefaultShape, SetDefaultLineColor, SetDefaultFillBrush [StartDrawing] [ProcessMouse] SetCapture, CopyMouseLocationToCurrentDrawObject PutDatalntoShape(PreviousDrawObject), PutDatalntoShape(CurrentDrawObject), InvalidateRect [StopDrawing] [StartTyping] [PositionTextString] [Processcharacter] ReleaseCapture InitializeTextData CopyMouseLocationToTextLocation, InitializeCaret MapVirtualKey, GetKeyState, AddCharacterToTextBuffer, GetTextRectangle, InvalidateRect, GetTextWidth, UpdateCaret [StopTyping] [TermlnputProcessing] TerminateCaret TerminateDrawingData ПРИМЕЧАНИЕ______________________________ Применение конечного автомата совместно с таблицей действий значительно повышает продуктивность программиста. Для более сложных автоматов части таблицы действий можно использовать повторно, так что не придется писать один и тот же код с нуля. Имена функций во второй колонке следуют в порядке их вызова. В список включены функции, принадлежащие абстракциям нижних уровней (например, DataMgr, DefaultMgr или DrawObjMgr), вспомогательные функции и вызовы Win32 API. Рассмотрим, к примеру, действие ProcessMouse. В таблице ниже для каждой функции из ассоциированного с ней списка указано, кто ее предоставляет. Имя действия Имя функции Компонент [ProcessMouse] PutDatalntoShape( PreviousDrawObject) PutDatalntoShape(CurrentDrawObject) InvalidateRect DrawObjMgr DrawObjMgr Win32 API Как видим, для реализации этого действия нужно обратиться к функциям из менеджера типов DrawObjMgr и Win32 API. В процессе написания кода список функций напрямую транслируется в текст программы.
Окончательное разбиение на уровни IIIIMMBH 205 СОВЕТ Применение конечного автомата совместно с таблицей действий повышает про- дуктивность программиста еще и потому, что позволяет написать легко читаемый код, который просто отлаживать, так как логика принятия решений наглядно представлена. Окончательное разбиение на уровни После включения в проект конечного автомата и таблицы действий вся про- грамма оказывается разбитой на уровни, с помощью которых легко обеспечить расширяемость и переносимость. Окончательный вариант изображен на рис. 7.8. Мы видим три четко выделенных уровня, каждый из которых инкапсулирует некоторые аспекты поведения программы и скрывает детали реализации от дру- гих уровней. □ Уровень пользовательского интерфейса. Механизмы взаимодействия с операционной системой Windows. □ Уровень принятия решений. Логика управления, сосредоточенная в ко- нечном автомате и таблице действий. □ Уровень управления данными. Доступ к данным через менеджеры объек- тов и типов для сокрытия истинной структуры данных. Каждый уровень реализуется конкретными компонентами. Реализация поль- зовательского интерфейса находится в диалоговой процедуре DlgProc. Те обра- ботчики сообщений, которые должны изменять состояние программы с помощью конечного автомата, например DlgOnMouseMove и DlgOnKeyDown, обращаются к уровню принятия решений. На этом уровне расположен новый компонент - UserlnputMgr. Самая важная функция в нем - ProcessUserlnput. Именно в ней находится конечный автомат и таблица действий. Компонент UserlnputMgr хра- нит переменную состояния Currentstate. Поскольку доступ к ней необходим только самому UserlnputMgr, то она хранится внутри него, а не под управлением DataMgr. Функции обработки, вызываемые из ProcessUserlnput, реализуют взаи- модействия с уровнем управления данными. Каждый из расположенных на этом уровне компонентов - DataMgr, DefaultMgr и DrawObjMgr - отвечает за опреде- ленные наборы данных. Об этом мы уже говорили выше. ПРИМЕЧАНИЕ Такое разбиение на уровни позволяет без труда реализовать конечный автомат и таблицудействий в рамках единственной функции ProcessUserlnput. Это упроща- ет отладку и повышает продуктивность. Кроме того, для изменения логики приня- тия решений нужно модифицировать только одну эту функцию, что благотворно сказывается на расширяемости. В качестве примера рассмотрим взаимодействия, которые происходят во вре- мя рисования эластичного контура. Вот их линейная последовательность.
206 'ШНИННШ Проектирование эффективных программ Рис. 7.8. Разбиение на уровни для управления состоянием пользовательского интерфейса □ Пользователь взаимодействует с уровнем пользовательского интерфейса. Сообщение WMM0USEM0VE поступает диалоговой процедуре DlgProc и передается обработчику DlgOnMouseMove. О Уровень пользовательского интерфейса взаимодействует с уровнем приня- тие решений. Обработчик вызывает функцию ProcessUserlnput, принадле
Процесс реализации I1IIIHB 207 жащую компоненту UserlnputMgr, передавая ей код сообщения в качестве аргумента. □ Уровень принятия решений взаимодействует с уровнем управления данны- ми. Основываясь на значении переменной CurrentState и коде сообщения, функция ProcessUserlnput обращается к следующим функциям: PutDatalntoShape(PreviousDrawObject) (предоставляется DrawObjMgr) PutDatalntoShape(CurrentDrawObject) (предоставляется DrawObjMgr) InvalidateRect (предоставляется Win32 API) Важно отметить, что любой компонент, расположенный на некотором уров- не, взаимодействует только с компонентами, расположенными ровно одним уровнем ниже. DlgProc не вызывает напрямую функции, которые предоставля- ет DrawObjMgr, поскольку такое взаимодействие обходит уровень принятия ре- шений, что могло бы привести к нежелательным последствиям. ПРИМЕЧАНИЕ Ограничение взаимодействий между уровнями уменьшает сложность програм- мы, а стало быть, и время отладки. Продуктивность программиста повышается, срок выхода на рынок сокращается. Процесс реализации Во время проектирования мы применили подход «сверху вниз» и получили разбиение программы на уровни и компоненты. На этапе реализации и тестирова- ния наилучшие результаты с точки зрения времени разработки дает подход «сни- зу вверх». ПРИМЕЧАНИЕ Если реализовывать проект, начиная с нижних уровней, то можно быть уверен- ным, что когда дело дойдет до верхних уровней, нижние уже будут отлажены. При подходе «снизу вверх» последовательность шагов реализации програм- мы рисования выглядит так: 1. Реализовать менеджер типов данных DrawObjMgr, который будет управ- лять несколькими экземплярами набора свойств для объекта DrawObjType. 2. Реализовать менеджер объектов DefaultMgr, который будет хранить един- ственный набор значений, необходимых для рисования, например вид те- кущей фи1уры и ширину линии. 3. Добавить к существующему компоненту DataMgr переменные и функции доступа для управления предыдущим и текущим объектами рисования, текстовым буфером и начальной точкой текста. 4. Добавить инкапсулированный менеджер CaretMgr для выполнения опера- ций с каре во время ввода / вывода текста.
208 Illi Проектирование эффективных программ 5. Реализовать UserlnputMgr для хранения переменной состояния и обработ- ки сообщений в соответствии с конечным автоматом и таблицей действий. 6. Модифицировать обработчики сообщений в DlgProc так, чтобы они взаи- модействовали с UserlnputMgr, а не занимались рисованием эластичного контура и вводом / выводом символов самостоятельно. 7. Расширить меню для выбора вида фигуры. 8. Изменить обработчик сообщения WM_COMMAND, чтобы он поддержи- вал новые пункты меню. 9. Добавить обработчик сообщения WM_INITMENUPOPUP, который будет отмечать пункт подменю, соответствующий последней выбранной фигуре. Если следовать этому плану, то компоненты уровня управления данными должны быть реализованы и отлажены первыми. Затем можно приступать к реа- лизации компонентов уровня принятия решения. Тогда на этапе интеграции с уровнем пользовательского интерфейса в нашем распоряжении уже будут отте- стированные компоненты нижних уровней. Один из описанных выше шагов заслуживает дополнительных замечаний. Компонент CaretMgr заменяет ранее использованный механизм на основе сооб- щения WM_POSITIONCARET. Для манипулирования таким ресурсом, как каре, функциональный подход более уместен и эффективен. Когда для этой цели при- меняется сообщение, программа вынуждена взаимодействовать с Windows для повторного входа в диалоговую процедуру. Накладные расходы при этом куда выше, чем при прямом вызове функции из небольшого компонента. Кроме того, такой подход лучше укладывается в разработанный проект. Анализ кода В этом разделе мы детально рассмотрим код, ориентируясь на описанные выше шаги реализации. Для каждого шага приводятся только фрагменты про- граммы, как правило, связанные со специфическими особенностями, обсуждав- шимися на этапе проектирования и повлиявшими на выбор подхода. Реализация менеджера типов данных DrawObjMgr Основной компонент уровня управления данными - это DrawObjMgr. И DataMgr, и UserlnputMgr пользуются предоставляемыми им средствами. Объявление этого абстрактного типа данных находится в двух файлах. В файле DrawObjDecl.h объявлена используемая структура данных, а в файле DrawObjMgr.h - абстрактный тип данных. * File: DrawObjDecl.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved.
Анализ кода Ullin I 209 typedef struct { RECT m_rect ; int m_lineWidth ; ShapeType m_shape ; int m_lineStyle ; int m_lineColor ; int m_fillBrush ; } DrawObjectRecordType ; /*********************************************** * File: DrawObjMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ finclude "DrawObjDecl.h" typedef DrawObjectRecordType * DrawObjectType ; Структура DrawObjectRecordType включает все атрибуты конкретного рису- емого объекта, в частности вид фигуры (m_shape), охватывающий прямоугольник (m_rect), и прочие. Но программист не работает с этой структурой. Вместо нее в программе используется логический тип данных DrawObjectType, объявленный во втором заголовочном файле. Он служит абстрактным представлением указате- ля на экземпляр описанной выше структуры. Приложение выполняет все опера- ции над структурой с помощью переменной этого абстрактного типа. ПРИМЕЧАНИЕ Эта реализация ведет себя, по существу, как класс C++. Но она написана на чистом С и гораздо более надежна. Прикладной программист даже не знает о существовании указателей. Разыменование происходит внутри методов досту- па, совершенно прозрачно для разработчика. Две из показанных на рис. 7.8 функций, работающих с этим типом данных, играют ключевую роль в разбиении на уровни. Их объявления находятся в файле DrawObj Mgr.h: void PutDatalntoShape(DrawObjectType DrawObject, int xl, int yl, int x2, int y2, ShapeType shape, int line_width, int line_style, int line_color, int fill_brush) ; Первым аргументом этой функции передается объект DrawObject. Это не- прозрачный указатель на соответствующую структуру данных. С точки зрения вызывающей программы функция выполняет операцию над экземпляром некоего абстрактного типа данных. Тот факт, что это на самом деле указатель и для мани- пулирования объектом его надо разыменовать, для программиста несуществен.
210 1111 Проектирование эффективных программ Оставшиеся аргументы задают значения свойств объекта, которые нужно инициа- лизировать. Поскольку этой и другим функциям, объявленным в DrawObjMgr.h, первым аргументом передается конкретный экземпляр типа DrawObjectType, то про- граммист может создать несколько объектов такого типа. Именно для этого DrawObjMgr сделан менеджером типов данных. void DrawShape(HDC DC, DrawObjectType DrawObject) ; Эта функция выполняет операции рисования. Для этого ей передаются кон- текст устройства DC и объект DrawObject, который надо нарисовать. Во время рисования учитываются свойства объекта, на который направлен переданный аб- страктный указатель. Чтобы понять, как эти функции скрывают структуру данных и само существо- вание указателей, взгляните на текст DrawShape: void DrawShape(HDC DC, DrawObjectType DrawObject) { switch (DrawObject->m_shape) { case LINESHAPE: DrawLineShapeAt( DC, DrawObj ect->m_rect.left, DrawObject->m_rect.top, DrawObject->m_rect.right, DrawObject->m_rect.bottom , DrawObject->m_lineWidth, DrawObject->m_lineStyle, DrawObject->m_lineColor ) ; break; ) // остальные ветви case опущены ) В этом фрагменте показаны все существенные особенности. Опущенные вет- ви устроены точно так же, поэтому приводить их код не имеет смысла. Предложение switch передает управление той ветви, которая соответствует ри- суемой фигуре. Ветвление происходит по полю m_shape. Чтобы получить его значе- ние, функция разыименовывает переданный указатель: DrawObject->m_shape. Внутри ветви case программа извлекает свойства объекта, которые нужно знать для рисования фигуры. Доступ к ним также производится путем разыиме- нования указателя на объект типа DrawObjectType. Значения свойств вместе с контекстом устройства передаются конкретной функции рисования. СОВЕТ ____________________________________— Функция DrawLineShapeAt является частью инкапсулированного компонента ри- сования, описываемого заголовочным файлом DrawOps.h. Напомним, что он впервые появился в главе 4 и повторно использован здесь. Повторное использование компонента DrawOps наглядно демонстрирует пользу инкапсуляции. К чему писать весь код заново, если он уже один раз был написан и надежно работает?
Анализ кода »!! 211 Реализация менеджера объектов DefaultMgr Поскольку этот компонент является менеджером объектов, то объявлений ти- пов для него нет. С точки зрения программиста, это не более чем набор функций, которые манипулируют полностью скрытыми данными. Вот часть заголовочного файла, в котором эти функции объявлены: ★ * File: DefaultMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ **************★********************************/ void SetDefaultShape(ShapeType shape) ; ShapeType GetDefaultShape(void) ; Эти функции доступны из любого места программы. Типы данных не видны, значит, данный компонент управляет единственным экземпляром фигуры по умолчанию. Обычно менеджер объектов хранит ровно по одному экземпляру пе- ременных одного или нескольких типов и предоставляет только методы доступа Get и Set. Но иногда программе нужно выполнять какие-то операции над инкап- сулированными данными. В этом случае компонент может предлагать и другие функции. Реализация вышеупомянутых функций приведена ниже: /*********************************************** ★ * File: DefaultMgr.с ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ***********************************************/ ♦include "DefaultMgr.h" static ShapeType static int static int static int static int defaultshape = LINESHAPE defaultLineWidth = 1 defaultLineStyle = PS_SOLID defaultLineColor = RGB(0,0,0) defaultFillBrush = WHITE_BRUSH // установить статические свойства фигур void SetDefaultShape(ShapeType shape) ( defaultshape = shape; } ShapeType GetDefaultShape(void)
212 1111 Проектирование эффективных программ return defaultshape; ) Поддерживаемое этим компонентом хранилище данных представляет собой набор переменных, объявленных с ключевым словом static. В языке С такие пере- менные видны только в том файле, где определены, а память для их хранения вы- деляется в статической области. Поэтому приложение может обратиться к ним только с помощью функций доступа. СОВЕТ Компилятор поддерживает инкапсуляцию за счет того, что скрывает статические переменные и вынуждает обращаться к ним с помощью методов доступа. Про- граммист не может обойти этот барьер, так как компилятор просто откажется компилировать код, нарушающий ограничения. Для пущей безопасности каждой скрытой переменной присваивается началь- ное значение. Эти значения переменные получают во время загрузки программы, то есть автоматически и ровно один раз. Две предыдущие функции - это простые методы доступа. Если программе нужно изменить текущий вид фигуры, она вызывает функцию SetDefaultShape Внутри функции это приводит к изменению значения переменной defaultshape Чтобы получить текущую фигуру, нужно вызвать функцию GetDefaultShape. ПРИМЕЧАНИЕ Вообще-то переменные, управляемые этим менеджером, могли бы находиться и в компоненте DataMgr. Однако здесь мы имеем дело с данными специального назначения, а не общего вида, как в DataMgr. Поэтому решили завести отдель- ный менеджер объектов только для них. Добавление переменных и методов доступа в компонент DataMgr В предыдущем варианте программы рисования было несколько переменных для поддержки рисования эластичного контура и обработки ввода / вывода сим- волов. Доступ к этим переменным необходим уровню принятия решений и уров- ню управления данными. Поэтому мы поместили их в компонент DataMgr. Вот исходный текст нового заголовочного файла DataMgr.h, из которого для краткости удалены некоторые объявления: у*********************************************** * File: DataMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved.
Анализ кода НИШ 213 void PutProgramlnstance(HINSTANCE Instance ) ; HINSTANCE GetProgramlnstance(void) ; void PutCurrentMouseLocation(int X, int Y) ; POINT GetCurrentMouseLocation(void) ; void CopyMouseLocationToCurrentDrawObject(void) ; void CopyMouseLocationToTextLocation(void) ; void InitializeTextData(void) ; POINT GetTextLocation(void) ; void PutTextLocation(POINT Location) ; int GetNumberCharacters(void) ; void PutNumberCharacters(int Number) ; void GetTextBuffer(TCHAR * Buffer) ; void PutTextBuffer(TCHAR * Buffer) ; void AddCharacterToTextBuffey(TCHAR Character) ; void InitializeDrawingData(void) ; void TerminateDrawingData(void) ; DrawObjectType GetPreviousDrawObject(void) ; DrawObjectType GetCurrentDrawObject(void) ; Функции доступа к описателю экземпляра программы PutProgramlnstance и GetProgramlnstance появились еще в минимальной диалоговой программе и с тех пор переходят из проекта в проект. Но все остальные функции новые. В этом варианте DataMgr есть функции двух видов. Простые методы доступа типа GetTextBuffer и PutTextBuffer дают возможность получать и устанавливать значения. Но такие функции, как InitializeDrawingData, выполняют сложные пос- ледовательности операций с данными. Несколько функций предназначены для работы с текущим положением мыши. Информация о нем поступает в программу из сообщений WM_MOUSEMOVE. Однако нужна она в разных местах и на разных уровнях. Поэтому обработчик DlgOnMouseMove обращается к функции PutCurrentMouseLocation, чтобы за- помнить полученные координаты курсора мыши. Когда где-нибудь в другом мес- те программе потребуются эти данные, она сможет получить их от функции GetCurrentMouseLocation. Иногда также возникает необходимость скопировать те- кущее положение мыши в другие переменные, внутренние для DataMgr. Чем тратить время на извлечение исходного и целевого объектов, а затем копировать данные из одного в другой, проще поручить копирование самому компоненту DrawMgr, для чего и предназначены функция CopyMouseLocationToCurrentDrawObject и ей по- добные. СОВЕТ _ Разрабатывая программы, предназначенные для Pocket PC, делайте все возмож- ное для ускорения работы программы, но не в ущерб целостности проекта. Добавление компонента CaretMgr В прежней реализации обработки ввода / вывода символов для управления каре использовалось нестандартное сообщение. Но этот подход несколько утоми-
214 III! Проектирование эффективных программ телен, поскольку необходимо определять код сообщения и писать для него анализа- тор. Кроме того, на запись сообщения в очередь и последующую выборку его в диалоговой процедуре тратится время. Эффективнее реализовать все это с помощью инкапсулированных функций. Соответствующий компонент предоставляет всего несколько методов, объявлен- ных в следующем заголовочном файле: ★ * File: CaretMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. * ***********************************************/ void InitializeCaret(HWND Window, int XLocation, int YLocation) ; void UpdateCaret(HWND Window, int XLocation, int YLocation) ; void TerminateCaret(HWND Window) ; Логически программа выполняет лишь три операции с каре: инициализацию, обновление и уничтожение. Для каждой из них существует отдельная функция. В коде нет никаких скрытых данных. Все функции работают с единственным каре, предоставляемым Windows. Ниже приведен текст функции UpdateCaret: void UpdateCaret(HWND Window, int XLocation, int YLocation) { HideCaret(Window) ; SetCaretPos(XLocation,YLocation) ; ShowCaret(Window) ; } Она выполняет точно такую же последовательность операций, что и обработ- чик нестандартного сообщения WM_POSITIONCARET из главы 5. Но в таком виде ее гораздо проще использовать повторно. Программисту достаточно скопиро- вать файлы с расширениями .h и .с в папку проекта, а затем включить файл с расширением .с в проект. После этого нужно будет лишь вставить в нужные места обращения к функции UpdateCaret. Реализация компонента UserlnputMgr для обработки сообщений Добавлением компонента CaretMgr мы завершили реализацию уровня управ- ления данными и переходим к уровню принятия решений - следующему в логичес- кой цепочке. Он включает всю логику конечного автомата и таблицу действий. Некоторые функции, находящиеся на этом уровне, также служат для управления рисованием в клиентской области. Основной программный элемент на этом уровне - компонент UserlnputMgr, содержащий ряд ключевых функций. Вот фрагмент его заголовочного файла:
Анализ кода 11111ПН 215 /*********************************************** * File: UserlnputMgr.h ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ************************************************/ typedef enum { Idle, Drawing, PreTyping, Typing } UserlnputStateType ; void ProcessUserlnput( HWND Window, UINT CurrentEvent ) ; void DisplayDrawObject(HDC DeviceContext) ; void DisplayTextString(HDC DeviceContext) ; Перечисление UserlnputStateType описывает возможные состояния взаимо- действия программы с пользователем. Поступающие конечному автомату сооб- щения передаются функции ProcessUserlnput. В обработчике сообщения WM_PAINT вызывается функция DisplayDrawObject, которая рисует эластич- ный контур, или функция DisplayTextString для поддержки ввода / вывода сим- волов. Вторым аргументом функции ProcessUserlnput передается переменная CurrentEvent типа UINT. Это может быть либо код сообщения, например WM_LBUTTONDOWN, либо виртуальный код клавиши, полученный в составе сообщения WMKEYDOWN. Можно было бы вместо этого передавать код сооб- щения и отдельно значение его параметра. Но принятый нами подход проще, и его легче реализовать. Изучить исходные тексты этих функций весьма полезно. Так, из кода ProcessUserlnput видно, как реализуются конечный автомат и таблица действий. Также этот пример ясно показывает, как осуществляется интеграция с уровнем управления данными. Вот несколько сокращенный текст: static UserlnputStateType Currentstate = Idle ; void ProcessUserlnput ( HWND Window, UINT CurrentEvent ) { RECT ShapeRect ; POINT MouseLocation ; DrawObjectType PreviousDO ; DrawObjectType CurrentDO ; ShapeType Shape ; int Linewidth ; int LineStyle ; int LineColor ; int FillBrush ; 11 Остальные объявления для краткости опущены if ( (Currentstate == Drawing) SS (CurrentEvent == WM_MOUSEMOVE) ) { // DataMgr PreviousDO = GetPreviousDrawObject() CurrentDO = GetCurrentDrawObject() ; MouseLocation = GetCurrentMouseLocation()
216 ИИ Проектирование эффективных программ // DrawObjMgr ShapeRect = GetShapeBoundingRect(CurrentDO) ; // DefaultMgr Shape = GetDefaultShape() ; Linewidth = GetDefaultLineWidth() ; LineStyle = GetDefaultLineStyle() ; LineColor - GetDefaultLineColor() ; FillBrush = GetDefaultFillBrush() ; PutDatalntoShape(PreviousDO, ShapeRect.left, ShapeRect.top, ShapeRect.right, ShapeRect.bottom, Shape, Linewidth, LineStyle, LineColor, FillBrush ) ; PutDatalntoShape(CurrentDO, ShapeRect.left, ShapeRect.top, MouseLocation. x, MouseLocation.y. Shape, LineWidth, LineStyle, LineColor, FillBrush ) ; InvalidateRect(Window,NULL,FALSE) ; UpdateWindow(Window) ; Currentstate = Drawing ; } // Остальной код опущен } Все тело функции - это обширный набор проверок переходов состояний. Каждая проверка реализована предложением if и представляет собой один пере- ход в диаграмме состояний. Код внутри каждого такого предложения должен выполнить шаги, определенные в таблице действий, а затем записать новое со- стояние в переменную CurrentState. В фрагменте выше показан переход из со- стояния «рисование» в то же самое состояние при поступлении сообщения WM_MOUSEMOVE. СОВЕТ „ Если бы такой переход не был реализован, то нельзя было бы нарисовать элас- тичный контур. Сборка и тестирование программы в таком случае вызвали бы немало затруднений. Код, реализующий любой переход, построен по единому принципу. if ( Currentstate == StateValue and CurrentEvent == Eventvalue ) then получить локальную копию необходимых данных от уровня управления данными выполнить необходимые операции над локальной копией передать новые значения данных уровня управления данными для сохранения Currentstate = NewStateValue end if Здесь ясно видна последовательность «ввод - обработка — вывод». Предвари- тельное получение данных необходимо для передачи входной инфо{ щи фун!
Анализ кода illHMM 217 циям, которые реализуют действия. Результаты выполненных операций, то есть выходную информацию, следует сохранить. Завершив ввод, обработку и вывод, программа обновляет внутреннюю переменную, чтобы отразить переход в новое состояние, которое в частном случае может совпадать с предыдущим. СОВЕТ Иногда последовательность «ввод - обработка - вывод» не отвечает требовани- ям в конкретной ситуации. Например, в предыдущем фрагменте последовательность операций, скорее, следовало бы описать как «ввод - вывод - обработка». Это связано с тем, что об- работка состоит из обновления клиентской области за счет рисования в ней объекта. Поэтому рисуемые объекты необходимо обновить путем вызова функ- ции PutDatalntoShape еще до обработки (то есть вызова функций InvalidateRect и UpdateWindow). Для получения локальных копий данных и сохранения выходной информа- ции необходимо взаимодействовать с уровнем управления данными. Даже если читатель незнаком детально с этим уровнем, сами имена Get, Set и Put наводят на мысль о работе с хранилищем данных. Отметим, что есть два вызова функции PutDatalntoShape, причем каждый раз ей передаются разные экземпляры DrawObjectType в качестве первого аргумента. Чтобы понять, как в этом фрагменте реализуется таблица действий, приведем еще раз ту ее часть, которая соответствует рассматриваемому переходу: Имя действия Имя функции Компонент [ProcessMouse] PutDatalntoShape(PreviousDrawObject) PutDatalntoShape(CurrentDrawObject) InvalidateRect DrawObjMgr DrawObjMgr Win32 API Видно, что последовательности вызовов функций в этой таблице и в тексте функции точно совпадают: PutDatalntoShape(PreviousDO, ShapeRect.left, ShapeRect.top, ShapeRect.right, ShapeRect.bottom, Shape, Linewidth, LineStyle, Linecolor, FillBrush ) ; PutDatalntoShape(CurrentDO, ShapeRect.left, ShapeRect.top, MouseLocation.x, MouseLocation.y. Shape, Linewidth, LineStyle, LineColor, FillBrush ) ; InvalidateRect(Window,NULL,FALSE) ; Тем самым становится очевидной связь между рис. 7.1 (диаграммой перехода состояний), табл. 7.1 (таблицей действий) и текстом функции ProcessUserlnput.
218 ШИ Проектирование эффективных программ ПРИМЕЧАНИЕ Прямой перевод спецификации, включающей диаграмму состояний и таблицу дей- ствий, в код программы существенно ускоряет тестирование программы, а стало быть, и сроки выхода на рынок. В результате выполнения последовательности InvalidateRect / UpdateWindow программа снова входит в обработчик DlgOnPaint сообщения WM_PAINT. Внут- ри него есть обращение к функции DisplayDrawObject, которая также является частью компонента UserlnputMgr. Ее назначение - обеспечить взаимодействие между уровнем пользовательского интерфейса и уровнем принятия решений. Вот полный текст функции DisplayDrawObject: void DisplayDrawObject(HDC DeviceContext) { DrawObjectType PreviousDO ; DrawObjectType CurrentDO ; if ( Currentstate == Drawing ) { // DataMgr * PreviousDO = GetPreviousDrawObject() ; CurrentDO = GetCurrentDrawObject() ; / / Стереть старую линию SetROP2(DeviceContext,R2_NOTXORPEN) ; DrawShape(DeviceContext, PreviousDO ) ; // Нарисовать новую линию SetROP2(DeviceContext,R2_COPYPEN) ; DrawShape(DeviceContext, CurrentDO ) ; } } Эта функция рисует эластичный контур. На первый взгляд, ей не место на уровне принятия решений. Но при внимательном изучении первой строки стано- вится ясно, что именно здесь ей и надлежит быть. Перед тем как приступать к ри- сованию контура, программа должна убедиться, что находится в состоянии «ри- сование», а для этого нужно проверить внутреннюю переменную. Следовательно, необходим доступ к этой - недоступной извне - переменной. Убедившись, что рисовать можно, функция получает локальную копию пре- дыдущего (PreviousDO) и текущего (CurrentDO) объектов рисования. Имея их и пользуясь бинарными растровыми операциями, программа может выполнить стирание и рисование. Чтобы стереть старый или нарисовать новый объект, функция сначала устанав- ливает растровую операцию SetROP2 (эта функция Win32 API была рассмотрена в главе 5). После этого обращение к DrawShape приводит собственно к стиранию или рисованию. Напомним, что функцию DrawShape предоставляет компонент DrawObjMgr, находящийся на уровне управления данными. Поскольку ей нужна информация о виде фигуры и стиле, хранящаяся в структуре типа DrawObjectType, то менеджер типов DrawObjMgr - для нее самое подходящее место.
Анализ кода 219 Модификация обработчиков в DlgProc для взаимодействия с UserlnputMgr После того как взаимодействия между уровнями принятия решения и управ- ления данными отлажены, можно переходить к следующему уровню. Наша цель на этом этапе - корректно реализовать взаимодействие между уровнями пользо- вательского интерфейса и принятия решений. Код уровня пользовательского интерфейса находится главным образом в диа- логовой процедуре DlgProc и в обработчиках сообщений. Взаимодействие с уров- нем принятия решений сводится к передаче сообщений от клавиатуры и мыши, а также сообщений WM_PAINT. В данном разделе мы проанализируем по одному примеру для каждой категории обработчиков. Первым рассмотрим обработчик сообщения WM_MOUSEMOVE: void DlgOnMouseMove(HWND hDlg, int x, int y, UINT keyFlags) { PutCurrentMouseLocation(x,у) ; ProcessUserlnput( hDlg, WM_MOUSEMOVE) ; } Вместо громоздкого кода, в котором участвуют несколько переменных состо- яния, эта функция состоит всего из двух строк. Сначала с помощью функции PutCurrentMouseLocation из компонента DataMgr регистрируется текущее поло- жение мыши, а затем вызывается функция ProcessUserlnput, которой передается код сообщения. Вся логика принятия решений и выполнения последовательности действий сосредоточена в уже отлаженном коде конечного автомата. Напомним, что сообщения WM_MOUSEMOVE поступают этому обработчи- ку при любом перемещении мыши по клиентской области. И в ответ на каждое сообщение вызывается ProcessUserlnput. Однако если предварительно пользова- тель не выполнил действий, приводящих к установке нужного состояния, то ProcessUserlnput просто игнорирует эти сообщения. ПРИМЕЧАНИЕ Использование диаграммы состояний и таблицы действий повышает надежность программы. За счет этого уменьшается время на отладку и количество ошибок, раздражающих пользователей. Далее мы приводим код обработчика сообщений WM_KEYDOWN: void DlgOnKeyDown(HWND hDlg, UINT vk, BOOL fDown, int cRepeat, UINT flags) { ProcessUserlnput( hDlg, vk ) ; } Реализация до смешного проста. Обработчик лишь передает код виртуальной клавиши vk функции ProcessUserlnput, принадлежащей уровню принятия реше-
220 ПК Проектирование эффективных программ ний. В предположении, что пользователь уже вошел в режим ввода текста, эта функция выполняет все действия по вводу / выводу символов. И последнее взаимодействие с уровнем принятия решений - обработчик DlgOnPaint сообщений WM_PAINT. Вот его сокращенный текст: void DrawingDlgOnPaint(HWND hDlg) { // Строки, рассмотренные выше, опущены DisplayDrawObject(DeviceContext) ; DisplayTextString(DeviceContext) ; // Строки, рассмотренные выше, опущены } Этот обработчик вызывает функции DisplayDrawObject и DisplayTextString, принадлежащие уровню принятия решений. Мы уже говорили, что им необходи- мо знать текущее значение переменной состояния. Поэтому помещение этих функ- ций именно на данный уровень вполне оправдано. Обратите внимание, как использование функции DisplayDrawObject скрыва- ет детали рисования эластичного контура. ПРИМЕЧАНИЕ За счет инкапсуляции операций и данных получается понятный код, четко отра- жающий логику работы программы. Код, написанный с использованием фраз, близких к естественному языку, в котором применяются высокоуровневые операции, легко отлаживать. Большая часть оставшихся ошибок связана с неправильным порядком выполнения инкап- сулированных операций. Сокрытие деталей манипулирования данными и внут- ренней реализации функций проясняет логику работы программы. Расширение главного меню Visual C++ и Embedded Visual C++ позволяют легко добавлять новые пункты меню и подменю. В программе уже есть пункт меню Quit, так что новые пункты мы добавим без особого труда. Процедура добавления пунктов меню проиллюстрирована на рисунках ниже. На каждом рисунке представлен один шаг. Пронумерованные надписи описыва- ют последовательность действий. На рис. 7.9 представлены основные этапы рас- ширения существующего меню. В обеих версиях Visual Studio имеются инструменты для редактирования меню. Чтобы войти в редактор, нужно проделать следующее. 1. Перейдите на вкладку Resource View в окне Project Explorer. 2. Щелкните по знаку + слева от папки Menu. В результате раскроется список меню. 3. Дважды щелкните по идентификатору главного меню приложения. Спра- ва от окна Project Explorer появится редактор меню. 4. Дважды щелкните по пустому месту справа от пункт1 ю Quit.
Анализ кода IIIIIM 221 2 . Раскрыть папку 3. Дважды щелкнуть по 4. Дважды щелкнуть На последнем шаге появляется окно свойств пункта меню, показанное на рис. 7.10. Первоначально в нем открыта вкладка General. Задайте свойства нового пунк- та главного меню следующим образом. 1. В поле Caption (Название) введите Shape. Это и будет название пункта меню. 2. Поставьте галочку слева от флажка Pop-up. При этом поле ID станет недо- ступным. 3. Щелкните по иконке с крестиком в правом верхнем углу окна, чтобы зак- рыть его. Для ниспадающих меню не нужны уникальные идентификаторы. Такие иден- тификаторы передаются диалоговой процедуре в составе сообщения, когда пользователь выбирает пункт меню. Но если речь идет о ниспадающем меню, то система сама открывает его, ничего не сообщая диалоговой процедуре. После того как окно свойств закроется, редактор разметит место для нового пункта меню. Следующим шагом мы должны определить пункты ниспадающего меню (рис. 7.11). Чтобы приступить к определению пункта ниспадающего меню, нужны всего два шага. 1. Щелкните по названию ниспадающего меню Shape. В результате появля- ется ниспадающее меню с одним пустым пунктом. 2. Дважды щелкните по пустому пункту ниспадающего меню.
222 Illi Проектирование эффективных программ Отметка флажка Pop-up деактивирует поле ID пункта меню 1. Введите название 3. Нажмите кнопку закрытия окна |мепи ItefnProfM’fties Extended Styles | “►P P^p-vp Г greyed ~~П Caption Г InacWe Г Help fiteek {None 2. Отметьте флажок Pop-up Рис. 7.10. Определение свойств нового пункта меню 1. Щелкните по названию ниспадающего меню Э-£3 DlgForm resources S-Qj Dialog Menu "&|ibf<.MENU21 2. Дважды щелкните по пустому пункту ниспадающего меню Рис. 7.11. Создание пунктов ниспадающего меню После первого шага появляются ранее определенные пункты ниспадающего меню. Если нужно поместить новый (пустой) пункт не в конец списка, перетащи- те его в нужное место. Затем дважды щелкните по пункту меню, чтобы открыть окно его свойств (рис. 7.12). Для описания нового пункта нужно выполнить следующие действия: 1. Введите название, например Line в поле Caption. 2. Не заполняйте поле ID, так как редактор меню автоматически присвоит ему уникальное значение.
Анализ кода {НИМ 223 4. Нажмите кнопку 2. Не заполняйте поле ID, редактор присвоит 1 • Введите ему значение автоматически название пункта закрытия окна ? Гб«егаГ*| Extended Styles | Р j" ▼ “ё| Caption > Г” Sepfi/Btaf Г Pop-vp Г* toacSve Break {None | Г Checked РгощрГ | Г” Grayed Г” Help 3. Не задавайте никаких других параметров Рис. 7.12. Задание свойств пункта ниспадающего меню 3. Не задавайте никаких других параметров. 4. Щелкните по иконке с крестиком в правом верхнем углу окна, чтобы за- крыть его. Ни в коем случае не задавайте никаких свойств на вкладке Extended Styles (Рас- ширенные стили). В программах для настольного ПК они работают, а на Pocket PC часто приводят к завершению программы без каких бы то ни было сообщений. После того как все пункты ниспадающего меню заданы, оно будет выглядеть, как показано на рис. 7.13. 1. Щелкните по пункту главного меню DlgForm.rc - IDR„MENU2 (Menu) 131 2. Дважды щелкните — по пункту ниспадающего меню Ouk Shape t I , *Д‘ ’-гтНЙ ------►One ; Rectangle | j Round Rectangle | Ellipse .{< ; Рис. 7.13. Ниспадающее меню с несколькими пунктами Для просмотра свойств определенного ранее пункта ниспадающего меню: 1. Щелкните по названию ниспадающего меню в главном меню. В результате появится ниспадающее меню; 2. Дважды шрткните по интересующему вас пункту ниспадающего меню.
224 1111 Проектирование эффективных программ Ha рисунке показано полностью сконфигурированное ниспадающее меню Shape. Свойства всех его пунктов определены, как описано выше. Напомним, что при задании свойств мы не указывали значение идентифика- тора пункта меню. Но, повторно открыв окно свойств, вы увидите, что идентифи- катор все-таки был создан самим редактором (рис. 7.14). В поле ID находится не числовое значение, а символическое имя. Редактор формирует его на основе положения данного пункта в иерархии меню. Чтобы выбрать из меню пункт, описание которого показано на рис. 7 14, пользователь должен будет сначала щелкнуть по пункту Shape главного меню, а потом по пункту Line ниспадающего меню. Имена этих пунктов объединяются, вначале добавляется префикс ID, и в результате мы получаем символическое имя ID_SHAPE_LINE. Где-то в недрах среды разработки имеется переменная, кото- рая отслеживает значение следующего уникального идентификатора и присваи- вает его данному символу. Редактор создает символическое имя для пункта ниспадающего меню (состоит из названия самого пункта плюс название соответствующего пункта главного меню) Рис. 7.14. Окно свойств ранее созданного пункта меню В результате всех этих действий Visual Studio создает два файла. В файле resource.h хранятся символические имена всех пунктов меню. Именно они ис- пользуются в программе, когда нужно распознать, какой пункт выбрал пользова- тель. Вот перечень символических имен для данного проекта. ♦define IDR_MENU2 101 ♦define ID_SHAPE_LINE 40001 ♦define ID_SHAPE_RECTANGLE 40002 ♦define ID_SHAPE_ROUNDRECTANGLE 40003 ♦define ID SHAPE ELLIPSE 40004 Visual Studio гарантирует, что все идентификаторы пунктов меню уникальны. Кроме того, для идентификаторов пунктов меню выделяется отдельный числовой диапазон. Помимо определений символических имен, Visual Studio еще генерирует сце- нарий, входящий в состав файла DlgForm гс. Этот файл компилируется в двоич- ную форму и включается в исполняемый файл программы, как говори тось в главе 2
Анализ кода 1111ИМ1 225 (рис. 2.4). Когда приложение хочет загрузить меню, функции Win32 API извлека- ют его описание из откомпилированного сценария. Ниже приведен фрагмент ресурсного сценария, в котором описано ниспадаю- щее подменю Shape: IDR_MENU2 MENU DISCARDABLE BEGIN MENUITEM "Quit", IDOK POPUP "Shape" BEGIN MENUITEM "Line”, ID_SHAPE_LINE MENUITEM "Rectangle", ID_SHAPE_RECTANGLE MENUITEM "Round Rectangle”, ID_SHAPE_ROUNDRECTANGLE MENUITEM "Ellipse”, ID_SHAPE_ELLIPSE END END Сначала указывается символическое имя меню IDR_MENU2. Все его пунк- ты описываются в блоке, следующем за этой строкой, в порядке следования пунктов. Первым идет пункт Quit. Его описание, начинающееся с ключевого сло- ва MENU ITEM, состоит из названия и символического идентификатора. Следующий элемент главного меню является ниспадающим меню, поэтому его описание начинается с ключевого слова POPUP, за которым следует название подменю. Далее идет вложенный блок с описанием пунктов ниспадающего меню. Внутри этого блока каждая строка начинается с ключевого слова MENU ITEM, за которым следуют название пункта и его символическое имя. Вообще говоря, программисту даже необязательно заглядывать в ресурсный сценарий, обо всех деталях позаботится Visual Studio или Embedded Visual Studio Модификация обработчика сообщения WM_COMMAND с учетом пунктов меню Когда пользователь выбирает какой-нибудь пункт ниспадающего меню, Windows посылает программе сообщение WM_COMMAND. В числе его парамет- ров есть уникальный целочисленный идентификатор выбранного пункта. Для реакции на такие сообщения нужно расширить обработчик DlgOn- Command. Во фрагменте ниже приведен код для обработки одного из пунктов ниспадающего меню. #include "resource.h" void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { // Рассмотренные ранее строки опущены switch( ilD ) { case ID_SHAPE_LINE: SetDefaultValues(LINESHAPE, 1, PS_SOLID, RGB(255,0,0), WHITE_BRUSH) ; break ; // Рассмотренные ранее строки опущены
226 ’1111 Проектирование эффективных программ Каждому пункту меню соответствует отдельная ветвь case в предложении switch. Управление ей передается в зависимости от значения аргумента ilD, кото- рый равен идентификатору выбранного пункта. Это то самое значение, которое сгенерировала Visual Studio. Метка в ветви case - не что иное, как константа, опре- деленная в файле resource.h. Для доступа к определениям этих констант заголо- вочный файл включен директивой #include. При компиляции программы вместо символического имени подставляется его числовое значение. В рассматриваемом приложении реакция на любое такое сообщение состоит в запоминании выбранной фигуры с предопределенным набором стилей. В следу- ющей главе мы разработаем более развитую программу, которая позволит самому пользователю задавать стили. Добавление обработчика WMJNITMENUPOPUP для индикации выбранной фигуры Когда пользователь щелкает по пункту главного меню, которому соответст- вует ниспадающее меню, Windows дает приложению возможность модифици- ровать свойства пунктов последнего. Для этого окну посылается сообщение WM_INITMENUPOPUP. Оно доставляется диалоговой процедуре и передается обработчику, если таковой существует. В нашем случае отмечается пункт меню, соответствующий ранее выбранной фигуре. Ниже показан фрагмент обработчика: void DlgOnlnitMenuPopup(HWND hDlg, HMENU hMenu, int item, BOOL fSystemMenu) { if ( GetDefaultShape() == LINESHAPE ) CheckMenuItem(hMenu,ID_SHAPE_LINE,MF_CHECKED) ; else CheckMenuItem(hMenu,ID_SHAPE_LINE,MF_UNCHECKED) ; // Остальной код для краткости опущен } Переданные обработчику аргументы идентифицируют источник сообщения. Второй аргумент hMenu содержит описатель ресурса родительского меню, владе- ющего тем ниспадающим меню, которое должно быть сейчас показано. Третий аргумент - порядковый номер выбранного пункта в родительском меню. В теле обработчика текущая выбранная фигура сопоставляется со всеми фигурами, представленными пунктами ниспадающего меню. Сопоставление со- стоит в сравнении текущей фигуры, которую обработчик получает от функции GetDefaultShape, с символическими константами, определенными в файле DrawObjTypes.h. Если числа совпали, отмечается соответствующий пункт меню. Функция Win32 API CheckMenuItem позволяет динамически ставить или убирать галочку против пункта меню. Вот пример ее вызова, взятый из обработ- чика DlgOnlnitMenuPopup: CheckMenuItem(hMenu, ID_SHAPE_LINE, MF_CHECKED) ; В первых двух аргументах передается описатель меню hMenu и целочислен- ный идентификатор пункта меню. Разумеется, вместо самого числа используется
Замечания по поводу проекта и реализацииЦЦММ1И1 227 символическое имя ID_SHAPE_LINE. Последний аргумент - флаг, описываю- щий нужную операцию. Если он равен MF_CHECKED, то галочка ставится, а если MF_UNCHECKED - убирается. Замечания по поводу проекта и реализации В реализации, описанной в настоящей главе, используются конечный автомат и таблица действий. При анализе конечного автомата было выявлено всего семь переходов состояний. Не исключено, что во время тестирования обнаружатся еще какие-то непредвиденные переходы. Для корректной работы программы необхо- дим полный и правильно реализованный конечный автомат. Чтобы удовлетво- рить требованию полноты, разработчику придется потрудиться. Если в автомате не будут отражены все возможные состояния и переходы, включая и возвратные, то программа будет вести себя непредсказуемо. Для описания полного конечного автомата и соответствующей таблицы дейст- вий необходимы дополнительные стадии в процессе разработки: □ идентифицировать основные варианты действий пользователя, которыми и определяются функциональные возможности приложения; □ проанализировать логику принятия решений и необходимую обработку, представив результат в виде диаграммы состояний, таблицы действий и диаграмм потоков данных; □ проверить полноту путем применения наложения диаграммы на различ- ные сценарии работы пользователя. Стадия проверки - ключевой этап. Здесь необходим очень строгий анализ, суть которого заключается в моделировании поведения системы при различных сценариях работы с ней путем применения логики принятия решений и функцио- нальной обработки. Реализация конечного автомата в виде последовательности проверок различ- ных условий, каждое из которых представляет собой сочетание текущего состоя- ния и входного сообщения, - подход не слишком удачный. Гораздо лучше описать автомат и таблицу действий в виде табличной структуры, которую затем инкапсу- лировать. Это повысит степень расширяемости, поскольку для изменения логики принятия решения будет достаточно модифицировать данные в таблицах, а не сам код, который еще нужно отлаживать. К сожалению, эта тема слишком обширна, и чтобы рассмотреть ее подробно, понадобилась бы отдельная книга. Резюме В этой главе программа из главы 5 полностью переработана с использованием разбиения на уровни. При этом мы пришли к следующим важным выводам. □ Вкладка Resource View в окне Project Explorer дает доступ к редактору меню, с помочи ю которого можно без труда добавить в программу меню.
228 III Проектирование эффективных программ □ Вообще говоря, на небольшом экране ниспадающие меню - не лучший спо- соб организации пользовательского интерфейса, поскольку они закрывают важную информацию. □ Менеджер объектов инкапсулирует доступ к одной или нескольким пере- менным. □ Менеджер типов данных инкапсулирует доступ к нескольким экземпля- рам скрытой структуры. □ За счет разбиения на уровни удалось выделить следующие компоненты программы: управление пользовательским интерфейсом, логику принятия решений и управление данными. □ Разбиение на уровни повышает степень расширяемости программы, по- скольку изменения, внесенные в код на одном уровне, остаются изолиро- ванными. □ Удачное разбиение на уровни ограничивает число взаимодействий между уровнями, а значит, уменьшает сложность программы и ускоряет ее разра- ботку. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание_______________________________________Папка______________ Программа работы с меню для настольного ПК MenuUsageProgram Программа работы с меню для Pocket PC MenuUsageProgramPPC Инструкции по сборке и запуску Программа работы с меню для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект MenuUsageProgram.dsw в папке MenuUsageProgram. 3. Соберите программу. 4. Запустите программу. 5. Выберите пункт главного меню Shape. Слева от пункта ниспадающего меню Line должна быть галочка. 6. Выберите какую-нибудь другую фигуру, например Rectangle. 7. Нарисуйте прямоугольник мышью, воспользовавшись методом эластич- ного контура. 8. Выберите из ниспадающего меню другую фигуру. При каждом появлении этого меню против ранее выбранного пункта должна быть поставлена га- лочка. 9. Нажмите клавишу BACKSPACE и щелкните левой кнопкой мыши. Долж- но появиться каре.
Примеры программ в Web ШИП 229 10. Введите строку символов. Каре должно перемещаться в позицию следую- щего символа. 11. Нажмите клавишу BACKSPACE. Каре должно исчезнуть. 12. Выберите пункт меню Quit. 13. Окно закроется, так как приложение завершило работу. Программа работы с меню для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект MenuUsageProgramPPC.vcw в папке MenuUsageProg- ramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу MenuUsageProgram. 12. Коснитесь стилосом пункта главного меню Shape. Слева от пункта ниспа- дающего меню Line должна быть галочка. 13. Выберите какую-нибудь другую фигуру, например Rectangle. 14. Нарисуйте прямоугольник мышью, воспользовавшись методом эластич- ного контура. 15. Выберите из ниспадающего меню другую фигуру. При каждом появле- нии этого меню против ранее выбранного пункта должна быть поставлена галочка. 16. Нажмите клавишу BACKSPACE и щелкните левой кнопкой мыши. Долж- но появиться каре. 17. Введите строку символов. Каре должно перемещаться в позицию следую- щего символа. 18. Нажмите клавишу BACKSPACE. Каре должно исчезнуть. 19. Выберите пункт меню Quit. 20. Окно закроется, так как приложение завершило работу.
Глава 8. Применение встроенных элементов управления в графическом интерфейсе пользователя В любой реальной программе для Pocket PC имеется развитый и иногда довольно сложный пользовательский интерфейс. На настольном ПК окно программы мо- жет занимать много места на экране. Но у Pocket PC маленький экран. В этой главе мы покажем, как можно уместить сложный интерфейс на ограниченной площади. Глава состоит из трех больших разделов. В первом разделе мы познакомимся со встроенными элементами управления, которые позволяют удобно организо- вать ввод информации. Когда пользователь взаимодействует с любым из встроен- ных элементов, Windows отправляет приложению сообщение. Функциональ- ность программы обеспечивают обработчики этих сообщений. Как все это увязывается в единое целое, мы и покажем в первом разделе. Второй раздел посвящен использованию полос прокрутки. Они позволяют вводить числа из заданного диапазона без клавиатуры, пользоваться которой на Pocket PC неудобно. И в последнем разделе мы специально займемся проблемой ограниченности физического экрана Pocket PC. Будут рассмотрены рисуемые владельцем кнопки и страницы со вкладками, обеспечивающие максимально эффективное использо- вание имеющегося пространства. Разработанные библиотеки позволят строить пользовательские интерфейсы с минимальными усилиями. ПРИМЕЧАНИЕ Начиная с этой главы, мы будем уделять основное внимание именно использова- нию библиотек, имеющихся на сайте http://vmv.osborne.com, тогда как ранее параллельно рассматривались библиотеки и объяснялись лежащие в их основе механизмы. Применение встроенных элементов управления в приложении Встроенные элементы управления - это компоненты интерфейса, доступ- ные любой программе для Pocket PC. Они имеют стандартный внешний вид, стандартный набор свойств и сообщений, описывающих, какое действие вы- полнил пользователь, а также набор методов для контроля над поведением эле- мента.
Применение встроенныхэлементовуправления Щ1Н1Н1ВИ 231 СОВЕТ .. ................................ Встроенные элементы управления доступны всем программам для настольных вер- сий Windows, на каком бы языке-C++, Visual Basic или Java-они ни были написаны. На рис. 8.1 перечислены все встроенные элементы управления, доступные программе на платформе Pocket PC. Каждый из них предназначен для определен- ной цели, а именно: Элемент управления Описание Статический текст Представляет метку, которая не может быть изменена пользователем Кнопка Позволяет пользователю уведомить программу о необходимости выполнить некое действие Переключатель Позволяет выбрать один из нескольких взаимоисключающих вариантов Флажок Однострочное поле ввода Многострочное поле ввода Список Позволяет выбрать несколько вариантов Позволяет ввести одну строку текста Позволяет ввести несколько строк текста Содержит ряд строк, из которых пользователь может выбрать одну или несколько Комбинированный список Объединение раскрывающегося списка и однострочного поля ввода. Позволяет выбрать один элемент из нескольких возможных Однострочное поле ввода Многострочное поле ввода Список Комбинированный список Рис. 8.1. Пользовательский интерфейс со встроенными элементами управления
232 Illi Применение элементов управления Из всех встроенных элементов чаще всего употребляются метка, комбиниро- ванный список и кнопка. ПРИМЕЧАНИЕ Пользуясь минимальной диалоговой программой из главы 3 в качестве образца, а также редактором ресурсов и панелью инструментов, имеющимися в Embedded Visual Studio, можно перетаскивать эти элементы управления на поверхность окна диалога. В типичном пользовательском интерфейсе метка описывает содержимое ком- бинированного списка. Чтобы раскрыть список, нужно коснуться стилосом рас- положенной справа стрелки, а затем выбрать из списка какое-нибудь значение, которое появится в однострочном текстовом поле слева от стрелки. Это действие служит для программы указанием на то, что необходимо выполнить некую опера- цию, используя значение в однострочном поле. Приложение, которое мы рассмотрим в этом разделе, преследует очень про- стую цель - продемонстрировать порядок работы с каждым из упомянутых эле- ментов. Обработчики сообщений от них будут выполнять в точности одно и то же. На рис. 8.2 показан типичный результат. Такое сообщение пользователь увидит, когда коснется кнопки, изображенной на рис. 8.1. Заголовок окна состоит из одного слова Status, а в текст сообщения говорит, от какого элемента поступило сообщение. Наконец, кнопка ОК позволя- ет убрать окно с экрана. Обзор встроенных элементов управления Каждому встроенному элементу управления соответствует предопределенный оконный класс. Эти классы Windows регистрирует в процессе начальной загрузки. ПРИМЕЧАНИЕ Напомним, что простая программа из главы 3 регистрировала оконный класс, а затем создавала окно этого класса. Оконный класс имеет ряд свойств, в частно- сти указатель на оконную процедуру, которая обрабатывает все поступающие окну сообщения. Помимо этих свойств, каждое окно обладает и другими, напри- мер стилями. Окно сообщения----► Status Button Pusfed -Информативное ।----сообщение IC.TZJ?K-- Л-*--Кнопка для закрытия ----------- окна Рис. 8.2. Это окно обработчик выводит в ответ на сообщение от встроенного элемента управления
Применение встроенных элементов управления ЦЦ1Н1Н1 233 На рис. 8.3 приведены имена классов, соответствующих всем встроенным эле- ментам управления. На первом уровне дерева находятся собственно имена классов. Так, кнопке соответствует класс с именем «BUTTON». Windows требует, чтобы имя класса указывалось точно, как написано, - заглавными буквами и в кавычках. Скажем, оконный класс списка должен называться «LISTBOX». С каждым классом связаны дополнительные уникальные флаги стилей Рис. 8.3. Класс и стили встроенных элементов управления С каждым классом элемента управления ассоциирован предопределенный набор стилей. Они задают общие для всех экземпляров этого класса внешний облик и поведение. Например, конкретное окно класса «BUTTON» может иметь один из следующих стилей: BS_PUSHBUTTON, BS_RADIOBUTTON или BS_CHECKBOX. При отображении эти стили определяют тот или иной внешний вид элемента (рис. 8.1). Помимо внешнего вида, стили определяют еще и поведение. Например, если окно имеет класс «BUTTON» и стиль BS_PUSHBUTTON, то при взаимодейст- вии с ним вид изменяется - кнопка выглядит нажатой. Кроме того, Windows по- сылает зарегистрированной оконной процедуре сообщение WM_COMMAND. Хотя для большинства встроенных элементов управления необходимо явно указывать стили, для некоторых стиль задается по умолчанию. К таковым отно- сятся классы «EDIT», «LISTBOX» и «СОМВОВОХ». Рассмотрим, например, элемент класса «LISTBOX». Когда программа создает такой элемент, его окно ав- томатически наделяется «однострочным» стилем, что позволяет выбрать из спис- ка только одну строку. Windows автоматически подсвечивает выбранную строку синим цветом. Окно класса «LISTBOX» получает такой стиль и поведение без всякого вмешательства со стороны программиста.
234 III Применение элементов управления ПРИМЕЧАНИЕ Перетащив элемент управления на поверхность диалога в редакторе ресурсов, разработчик может явно задать его стили. Для этого нужно дважды щелкнуть по элементу, в результате чего откроется окно свойств. Когда пользователь взаимодействует со встроенным элементом управления, Windows посылает сообщения оконной процедуре, зарегистрированной для клас- са этого элемента. Та отвечает на сообщения строго определенным образом. На рис. 8.4 представлена вся последовательность обработки сообщения. Рис. 8.4. Отношения между родительским и дочерним окном Любой элемент управления располагается в каком-то родительском окне. В ходе взаимодействия с пользователем между оконными процедурами родитель- ского окна и окна элемента происходит обмен сообщениями, как показано на рис. 8.4. Последовательность событий такова. 1. Пользователь касается кнопки стилосом. 2. Windows посылает сообщение WM_COMMAND скрытой оконной проце- дуре, зарегистрированной для класса «BUTTON». 3. Скрытая оконная процедура переправляет сообщение WM COMMAND известной оконной процедуре родительского окна. 4. Обработчик сообщения WMCOMMAND в процедуре родительского окна отвечает на сообщение так, как приложение находит нужным. 5. Если родительское окно захочет послать команду с целью изменить значе- ние некоторого свойства, то должно будет сконструировать и отправить предопределенное сообщение скрытой оконной процедуре элемента управ- ления. Для передачи сообщения другому окну применяется функция SendMessage.
Применение встроенных элементов управления 235 6. Чтобы запрошенные изменения возымели действие, скрытая оконная про- цедура отправляет необходимые команды Windows. Скрытая оконная процедура каждого из классов элементов управления отве- чает на заранее определенный набор сообщений. Эти сообщения могут исходить от Windows (например, сообщение WM_COMMAND для класса «BUTTON») или от прикладной программы, которая отправляет их с помощью функции SendMessage. Все скрытые оконные процедуры входят в состав компонента WINDOW, которым был рассмотрен в главе 1. Перечень сообщений для каждой такой процедуры вместе с описанием параметров включен в состав оперативной справки, поставляемой с Embedded Visual Studio В табл. 8.1 приведен список сообщений, посылаемых встроенными элемента- ми управления. Здесь же показано, какие сообщения нужно посылать элементам для чтения или изменения их свойств. Список в табл. 8.1 конечно же неполон. Исчерпывающий список занял бы слишком много места. Но перечисленные сообщения применяются наиболее часто. Зная их, можно уже приступать к программированию встроенных эле- ментов. Сообщения, перечисленные в табл. 8.1, определяют порядок двустороннего взаимодействия между родительским окном и дочерним окном элемента управле- ния. Когда пользователь что-то делает с элементом, его скрытая оконная процеду- ра посылает соответствующее сообщение родительскому окну. Для того чтобы получить значение свойства элемента, родительское окно посылает ему одно из сообщений, находящихся в колонке «Чтение свойства», а чтобы изменить свой- ство - одно из сообщений в колонке «Изменение свойства». Значения в этих ко- лонках - символические имена, которые можно использовать в программе при условии, что включен заголовочный файл windows.h. Рассмотрим, к примеру, элемент управления класса «LISTBOX». Когда пользователь выбирает строку из списка, скрытая оконная процедура отправляет родительскому окну сообщение LBN_SELCHANGE. Обработчик этого сообще- ния в оконной процедуре родительского окна с помощью функции SendMessage посылает элементу сообщение LB_GETCURSEL, чтобы получить индекс выб- ранной строки (индексация начинается с нуля). Затем он может использовать по- лученную информацию, как пожелает. К сожалению, разработчики этой части Windows были не слишком последова- тельны. Иногда для получения свойств элемента нужно вызывать функции, а не посылать сообщения. Примером может служить элемент класса «EDIT». Для чтения и установки текста в поле ввода предназначены функции GetMessageText и SetMessageText, никаких сообщений для этой цели не предусмотрено. Такой подход не только непоследователен, но и вносит путаницу. Обычно эти функции применяются для задания заголовка окна. Здесь же они служат для доступа к со- держимому клиентской области окна. Но поскольку Windows так желает, у про- граммиста не остается выбора.
Таблица 8.1. Сообщения от элементов управления и чтение / изменение их свойств Элемент управления Класс Отправляемые Изменение сообщения Чтение свойства свойства Кнопка «BUTTON» WM_COMMAND BM_SETSTATE BM GETSTATE Флажок «BUTTON» WM COMMAND BM SETCHECK BM GETCHECK Комбинированный список «СОМВОВОХ» WM_COMMAND, CB_ADDSTRING CB_GETCURSEL CBN_SELCHANGE CB_GETLBTEXT Поле ввода «EDIT» WM_COMMAND, SetWindowText GetWindowText enJkillfocus EM_SETSEL EM_REPLACESEL Статический текст «STATIC» Нет SetWindowText GetWindowText Список «LISTBOX» WM_COMMAND, LB_ADDSTRING LB_GETCURSEL LBN SELCHANGE LB_GETTEXT Переключатель «BUTTON» WM_COMMAND BM_SETCHECK BM_GETCHECK ПН Применение элементов управления
Применение встроенных элементов управления |||ИН1Ш^Н 237 Реализация интерфейса со встроенными элементами управления Для включения встроенного элемента управления в интерфейс программы нужно выполнить следующие действия: 1) добавить элемент в интерфейс; 2) инициализировать состояние элемента; 3) добавить обработчик сообщения, посылаемого в результате действий пользователя. С помощью имеющихся инструментов все это можно сделать за пару минут. На сайте этой книги имеется программа IntrinsicControlsProgram, в которой описанные шаги реализованы для каждого элемента управления на рис. 8.1. Ниже мы рассмотрим только работу со списком. Добавление элемента управления в пользовательский интерфейс Поскольку основой любого приложения является диалог, то добавление списка в ее интерфейс оказывается совсем простой задачей. На плавающей панели инстру- ментов в редакторе диалогов представлены все встроенные элементы управления. Перетащите нужный (в нашем случае список) в подходящее место в окне диалога. Затем с помощью мыши вы можете уточнить положение и размеры элемента. ПРИМЕЧАНИЕ Простота добавления и перемещения элементов управления - одно из основных достоинств применения диалогов в программах для Pocket PC. Можно создавать новые элементы и программно с помощью функции Win32 API CreateWindowEx, указав соответствующий оконный класс, например «LISTBOX». Но чтобы увидеть, как расположился элемент, придется запустить программу, затем уточнить положение и размеры и снова запустить. Этот итера- тивный процесс занимает много времени, на разработку сложного интерфейса ре- альной программы легко может уйти несколько дней. Применение же редактора диалогов сокращает время до нескольких минут. Инициализация элемента У каждого встроенного элемента управления есть внутреннее состояние, а у не- которых - еще и данные. Обычно начальное состояние задает определенный внеш- ний вид элемента. Например, кнопка должна быть первоначально отжата, а не нажата. Дополнительные данные есть у таких элементов, как «LISTBOX» и «СОМВОВОХ». В ходе инициализации можно поместить в список некоторые строки. ПРИМЕЧАНИЕ Выполнять инициализацию элементов управления следует в обработчике сооб- щения WMJNITDIALOG, который вызывается до отображения диалогового окна.
238 III! Применение элементов управления Порядок инициализации списка продемонстрирован в листинге ниже: #include <windowsx.h> BOOL OnlnitDialog ( HWND hDlg , HWND hDlgFocus , long llnitParam ) ( HWND Control ; Control = GetDlgltem(hDlg, IDC_LIST1) ; ListBox_AddString(Control, __TEXT(«Item 1»)) ; ListBox_AddString(Control, __TEXT(«Item 2»)) ; } В заголовочном файле windowsx.h находятся макросы, обертывающие обра- щения к функции SendMessage. Выше уже было сказано, что эта функция позво- ляет родительскому окну контролировать поведение встроенного элемента управ- ления. Макрос ListBox_AddString вызывает функцию SendMessage с такими аргументами, которые приводят к добавлению новой строки в конец списка. Чтобы добавить строку в список, обработчик должен сначала получить описа- тель окна списка. Функция GetDlgltem возвращает этот описатель, зная описа- тель родительского окна и идентификатор ресурса IDC_LIST1, присвоенный ре- дактором в Visual Studio. СОВЕТ Любой встроенный элемент управления - это окно предопределенного класса. С классом связана оконная процедура, которую Windows регистрирует во время начальной загрузки. Получив описатель окна списка, обработчик сообщения заполняет список. Макрос ListBox_AddString вызывает функцию SendMessage для отправки сооб- щения LB_ADDSTRING окну, описатель которого передан в аргументе Control. При этом новая строка добавляется в конец списка. Существует также макрос ListBox_InsertString, который добавляет строки, поддерживая список отсортиро- ванным по возрастанию или убыванию в зависимости от значения некоторого свойства, задаваемого из программы еще до вставки в список первой строки. Добавление обработчика сообщения, посылаемого в результате действий пользователя Когда пользователь взаимодействует с элементом управления, программа дол- жна адекватно реагировать. В случае выбора из списка скрытая оконная процедура отправляет родительскому окну сообщение WM_COMMAND. Одним из его пара- метров является код извещения, равный LBN_SELCHANGE. Эта константа опре- делена в файле windows.h и означает, что в списке выбрана другая строка. ПРИМЕЧАНИЕ Любой встроенный элемент управления генерирует по крайней мере одно сооб- щение, предназначенное родительскому окну. Иногда в него нужно включать код извещения, чтобы оконная процедура могла правильно интерпретировать сооб- щение. А иногда код извещения не имеет значения.
Применение встроенныхэлементовуправления ]||ЦН1М 239 Сообщение об изменении выбранной строки обрабатывается следующим об- разом: void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) ( switch) ilD ) ( case IDC_LIST1: if ( uCodeNotify == LBN_SELCHANGE ) { Selectedltem = ListBox_GetCurSel(hDlgCtl) ; ListBox_GetText(hDlgCtl, Selectedltem, RawText) ; MessageBox(hDlg,RawText,_TEXT("Status"),MB_OK) ; } break ; } } Первым делом обработчик проверяет, что источником сообщения является известный ему список. Для этого аргумент ilD сравнивается с идентификатором IDC_LIST1. Далее он по коду извещения отфильтровывает все сообщения, кроме LBN_SELCHANGE, поскольку заинтересован лишь в событиях изменения вы- бранной строки. Чтобы узнать, какую строку выбрал пользователь, обработчик посылает запрос списку. Описатель списка hDlgCtl передается в качестве одного из параметров запроса. Для получения значения выбранной строки нужно два шага. Сначала функция SendMessage, обернутая макросом ListBox_GetCurSel из файла windowsx.h, возвращает индекс выбранной строки. А затем этот индекс пере- дается макросу ListBox_GetText, который и возвращает значение строки. Далее приложение вольно использовать полученную информацию, как ему будет угодно. Пользователь может взаимодействовать с разными элементами управления, а программа всякий раз должна адекватно реагировать.'Иногда действия пользо- вателя приводят к изменениям в видимом состоянии элемента. За модификацию состояния отвечает обработчик сообщения. Бывает и так, что взаимодействие с одним элементом приводит к изменению состояния сразу нескольких других элементов. К вопросу о переносимости Если заглянуть в ресурсный сценарий IntrinsicControlsProgram.rc, то в нем обнаружатся два диалога. Это необходимо для обеспечения переносимости между Windows и Windows СЕ. Открыв оба диалога в редакторе ресурсов Visual Studio, вы сразу увидите, в чем отличие. Оба диалога содержат в точности одни и те же элементы. Но в диа- логе IDD DIALOG2 они смещены вниз по сравнению с IDD_DIALOG1.
240 Illi Применение элементов управления ПРИМЕЧАНИЕ В версии Windows для настольного ПК все элементы управления автоматически смещаются в клиентской области вниз с учетом высоты полосы меню. Но версия Windows для Pocket PC 2002 ничего такого не делает, поэтому разработчик дол- жен позаботиться об этом сам. Таким образом, чтобы приложение можно было легко перенести с настольно- го ПК на Pocket PC 2002, необходимы два разных диалога. Диалог IDD_DIALOG1 используется в версии для настольного ПК, а диалог IDD_DIALOG2 - в версии для КПК. Для переключения с одной платформы на другую достаточно изменить значе- ние константы WindowsCE в файле IFiles.h. СОВЕТ Назначение и порядок применения флага WindowsCE описаны в главе 3. Этот флаг управляет выбором диалога при отображении главного окна прило- жения. Нужно лишь немного модифицировать функцию DlgMain: int WINAPI WinMain( HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR IpCmdLine, int nCmdShow) { PutProgramlnstance(hlnstance) ; #if WindowsCE DialogBox( hlnstance, MAKEINTRESOURCE(IDD_DIALOG2), HWND_DESKTOP, (DLGPROC) DlgProc ) ; #else DialogBox( hlnstance, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, (DLGPROC) DlgProc ) ; #endif return 0 ; } Выбором нужной ветви управляют директивы препроцессора #if, #else и #endif. Если константа WindowsCE равна 1, компилируется первое предложение и, следовательно, отображается диалог IDD_DIALOG2. Если же она равна 0, ото- бражается диалог IDD_DIALOG1. Использование групп элементов управления для реализации дружелюбного интерфейса Одна из значимых задач интерфейса - дать пользователю возможность вво- дить числовые данные. Для программы, работающей на платформе Pocket PC, удобный способ ввода чисел особенно важен.
Использование групп элементов управления ЩННИИ^В 241 ПРИМЕЧАНИЕ h, ijin initinimjiiiHtiigiuimgHliOliwnginff', j,^^c^TTii[rio.inwig8gaat»agWHWg!MMgg№JHHgWtfi'.1iii'i . •.".4gl.'...iu '.!.’.f i> v Из-за малой площади экрана КПК программа должна предоставить способ ввода чисел без использования клавиатуры. Клавиатура на Pocket PC частично пере- крывает окно приложения, к тому же для работы с ней нужен стилос. Вместе взя- тое, это повышает вероятность ошибок при вводе данных. Кроме того, обычно требуется, чтобы вводимые числа принадлежали ограни- ченному диапазону, и программа должна это контролировать. Для Windows-приложений, рассчитанных как на настольный ПК, так и на КПК, для ввода числовых данных лучше всего подходит встроенный элемент «по- лоса прокрутки». С ее помощью можно представить непрерывный ограниченный диапазон. Но применять ее следует вместе с некоторыми другими элементами. Применение полосы прокрутки в паре с полем ввода К сожалению, многие приложения используют полосу прокрутки совершенно неправильно. Часто не указывается допустимый диапазон. Хуже того, не предо- ставляется возможности уточнить значение, введенное с помощью полосы про- крутки, воспользовавшись при необходимости клавиатурой. В этом разделе мы покажем, как с помощью полосы прокрутки и некоторых других элементов можно организовать эффективный ввод числовых данных. Программа будет контролировать корректность числового значения и его попада- ние в допустимый диапазон. В паре с полосой прокрутки используется поле ввода. В нем отображается значение, соответствующее текущему положению ползунка в полосе прокрутки. А при желании пользователь сможет ввести число непосред- ственно в это поле. Если данные вводятся с клавиатуры, то программа будет кон- тролировать правильность и обновлять положение ползунка. На рис. 3.5 показано, как работает группа элементов с полосой прокрутки во главе. У пользователя есть четыре способа ввести значение. □ Буксировка ползунка позволяет представить любое значение из допусти- мого диапазона. Приращение во время буксировки довольно велико и опре- деляется самой системой Windows. □ Если коснуться стилосом свободного места в полосе прокрутки, то ползунок смещается на величину, определяемую параметром PAGE_INCREMENT, задать которую можно, обратившись к функции Win32 API. □ Касание любой стрелки сдвигает ползунок ровно на одну единицу. □ При вводе числового значения в поле справа от полосы прокрутки с помо- щью клавиатуры и стилоса ползунок устанавливается в соответствующую позицию. Программа проверяет, что введено число, попадающее в допус- тимый диапазон. В следующем разделе мы запрограммируем эту процедуру с помощью специ- ально разработанных функций, которые облегчат включение полосы прокрутки и дополнительных элем итов в программу.
242 Illi Применение элементов управления Щелкнуть по стрелке. Сдвиг на одну единицу Ввести число с помощью клавиатуры Буксировать ползунок Щелкнуть по свободному месту. Сдвиг на PAGEJNCREMENT единиц Рис. 8.5. Операции с помощью полосы прокрутки и дополнительных элементов Описанный подход требует совместной работы нескольких встроенных элементов управления. На рис. 8.6 показано, какие элементы участвуют в про- цессе. В организации «дружелюбной» полосы прокрутки участвует ровно пять встроенных элементов управления. Сверху находится метка, в ней отображается имя переменной, значение которой задается с помощью данного элемента. Сразу под ней расположена сама полоса прокрутки, а справа от нее - поле ввода, которое решает двоякую задачу. Когда пользователь что-то делает с полосой прокрутки, в этом поле отображается текущее значение, определяемое положением ползунка. Но можно и напрямую ввести в него значение с помощью клавиатуры и стилоса. Наконец, по обе стороны от полосы прокрутки размещены еще две метки, показы- вающие нижнюю и верхнюю границы допустимого диапазона. Полоса прокрутки автоматически препятствует выходу за эти границы. Если же значение вводится в текстовое поле, то попадание в диапазон контролирует программа. ПРИМЕЧАНИЕ Типичное диалоговое окно на Pocket PC, заполняющее весь экран, может вмес- тить ровно три такие группы. Если программе нужно более трех числовых пере- менных, то придется с помощью вкладок организовать иерархический интер- фейс. О том, как это делается, мы расскажем в главе 9.
Использование групп элементов управления jЦНННМ1 243 Метка полосы прокрутки Парное поле ввода Метка с минимально допустимым значением Полоса прокрутки Метка с максимально допустимым значением Рис. 8.6. Дружелюбная полоса прокрутки Когда пользователь взаимодействует с полосой прокрутки или парным полем ввода, программа получает поток сообщений. Кроме того, действия с каждым из этих элементов влияют на состояние другого. На рис. 8.7 показано, как связаны между собой манипуляции пользователя, генерация сообщений и состояние от- дельных элементов. Цифрами обозначен порядок действий, выполняемых во время работы про- граммы. 1. Пользователь манипулирует полосой прокрутки, касаясь стилосом пол- зунка, свободного места или стрелок. 2. Скрытая оконная процедура полосы прокрутки посылает родительскому окну сообщение WMHSCROLL. 3. Обработчик сообщения WM_HSCROLL в диалоговой процедуре запоми- нает текущее значение, представленное полосой прокрутки, и, возможно, положение ползунка. 4. Зная текущее положение ползунка, обработчик вычисляет значение пере- менной и отображает его в поле ввода. 5. Вместо этого пользователь может сам ввести значение в поле ввода с помо- щью виртуальной клавиатуры и стилоса. 6. После ввода каждого символа скрытая оконная процедура поля ввода по- сылает родительскому окну сообщение WM_COMMAND. Код извещения в нем равен символической константе EN_CHANGE, определенной в фай- ле windows, h.
244 Illi Применение элементов управления 5. Пользоввтель взаимодействует с полем ввода 1. Пользователь взаимодействует с полосой прокрутки Рис. 8.7. Взаимосвязь между обновлением полосы прокрутки и парного поля ввода 7. В диалоговой процедуре родительского окна обработчик сообщения WM_COMMAND считывает и проверяет значение, находящееся в поле ввода. Контролируется, что это число, к тому же принадлежащее допусти- мому диапазону. 8. Если значение корректно, обработчик устанавливает ползунок в соответ- ствующее положение. Разумеется, все эти детали скрыты в нескольких библиотечных функциях, ко- торые позволяют за пару минут включить в интерфейс программы описанный со- ставной элемент управления. Включение дружелюбной полосы прокрутки Все библиотечные функции для поддержки дружелюбной полосы прокрутки находятся в компоненте GUIUtils. Они инкапсулируют как инициализацию, так и работу с группой составляющих ее встроенных элементов. Чтобы включить составной элемент в свою программу, необходимо выпол- нить следующие действия. 1. С помощью редактора диалогов поместить все пять элементов в форму. 2. Включить в диалоговую процедуру заголовочный файл GUIUtils.h, кото- рый дает доступ к нужным функциям. 3. Объявить и инициализировать переменные, в которых будут храниться границы диапазона и текущее положение ползунка. 4. В обработчике сообщения WM_INITDIALOG инициализировать полосу прокрутки и парное поле ввода. 5. Добавить ветвь case для обработки сообщения WM_COMMAND от поля ввода. 6. Добавить в диалоговую процедуру обработчик сообщения WM_HSCROLL. На сайте книги имеется программа ScrollBarControlProgram, которая реали- зует все описанные шаги. Ниже мы вкратце рассмотрим ее код.
Использование групп элементов управления ]|||НИНИ1 245 Использование редактора диалогов для помещения пяти элементов в форму На этом шаге разработчик перетаскивает пять элементов управления с панели инструментов в диалог и с помощью мыши располагает их в нужном порядке, как показано на рис. 8.6. ПРИМЕЧАНИЕ ———————И—————Illi—«fc—Wlllllll,l’MtWllllllH|imiTt1lllltlil1ll!l» КПП—«ММ———MM——————I—ям— То, что программист сразу же видит взаимное расположение отдельных элемен- тов, -дополнительное свидетельство в пользу подхода, основанного на приме- нении диалогов. Включение файла GUIUtils.h в диалоговую процедуру Все необходимые функции объявлены в заголовочном файле, который вклю- чается следующей директивой: #include "GUIUtils.h" Теперь они станут видны различным обработчикам, вызываемым из диалого- вой процедуры. Объявление и инициализация переменных Переменные, которые понадобятся для работы со вспомогательными функци- ями, объявлены в следующем фрагменте: «define SCROLL_VALUE_MIN 0.0 «define SCROLL_VALUE_MAX 90.0 «define SCROLL_NUMBER_INCREMENTS_PER_UNIT 1 «define SCROLL_NUMBER_FRACTIONAL_DIGITS 0 double SCROLL_CurrentScrollValue = SCROLL_VALUE_MIN ; int SCROLL_CurrentScrollPos = 0 ; BOOL SCROLL_ProcessEdit = TRUE ; BOOL SCROLL_BuddyInitialized = FALSE ; Константы SCROLL_VALUE_MIN и SCROLL_VALUE_MAX определяют гра- ницы допустимого диапазона значений вводимой переменной. Значение может быть дробным, поэтому константа SCROLL_NUMBER_INCREMENTS_PER_UNIT определяет, сколько дробных долей приходится на один шаг перемещения пол- зунка. Например, если она равна 10, то можно будет вводить значение с точностью до одной десятой. Константа SCROLL_NUMBER_FRACTIONAL_DIGITS равна числу цифр после запятой при вводе числа с клавиатуры. Позиция ползунка изменяется от 0 до 100. В переменной SCROLL_Cur- rentScrollPos хранится его текущая позиция. В ходе обработки по этому значению и с учетом констант SCROLL_VALUE_MIN и SCROLL_VALUE_MAX вычисля- ется значение, представленное полосой прокрутки. Оно и запоминается в пере- менной SCROLL_CurrentScroll Value.
246 Illi Применение элементов управления Между полосой прокрутки и парным полем ввода существует тонкая связь. Когда пользователь буксирует ползунок, обработчик сообщения WM_HSCROLL обновляет поле ввода. Но при этом скрытая оконная процедура поля ввода гене- рирует сообщение WM_COMMAND. И его обработчик попытается обновить по- зицию ползунка, что может привести к бесконечному циклу. Разорвать такой цикл помогает общая для обоих обработчиков переменная SCROLL_ProcessEdit. Приложение должно инициализировать ее, а потом передать указатель на нее од- ной из функций, входящих в компонент GUIUtils. Все последующие манипуля- ции с этой переменной производятся внутри самого компонента, и приложению до них нет дела. И последняя переменная SCROLL_BuddyInitialized позволяет обойти весь код обработки сообщений WM_COMMAND, приходящих от скрытой оконной проце- дуры парного поля ввода, до тех пор, пока не будет завершена инициализация поло- сы прокрутки в обработчике сообщения WM_INITDIALOG. Без нее поле ввода могло бы быть инициализировано значением, выходящим за пределы диапазона. ПРИМЕЧАНИЕ Имена всех переменных начинаются со слова SCROLL. В ходе включения друже- любной полосы прокрутки в свою программу разработчик может скопировать по- казанные выше строки, а затем с помощью глобальной замены преобразовать имена в более осмысленные. Однако выполнять замену нужно после завершения всех остальных шагов, так как эти переменные передаются функциям из компо- нента GUIUtils. Инициализация элементов в обработчике сообщения WMJNITDIALOG В следующем фрагменте функции InitializeScrollAndBuddy передаются иден- тификаторы встроенных элементов управления и объявленные выше переменные для инициализации дружелюбной полосы прокрутки. По завершении инициали- зации обработчик устанавливает флаг SCROLL_BuddyInitialized в TRUE, чтобы впоследствии нормально обрабатывались все сообщения WM_COMMAND от парного поля ввода. InitializeScrollAndBuddy(hDlg, IDC_SCROLLBAR1, SCROLL_VALUE_MIN, SCROLL_VALUE_MAX, SCROLL_NUMBER_INCREMENTS_PER_UNIT, IDC_EDIT1, IDC_STATIC1, IDC_STATIC2, SCROLL_NUMBER_FRACTIONAL_DIGITS, SCROLL_CurrentScrollValue, &SCROLL_CurrentScrollPos ) ; SCROLL_BuddyInitialized = TRUE ; Здесь IDC_SCROLLBAR1 - идентификатор полосы прокрутки, IDC_EDIT1 - идентификатор парного поля ввода, a IDC_STATIC1 и IDC_STATIC2 - иденти- фикаторы меток, содержащих минимальное и максимальное значения.
Использование групп элементов управления ЦЦНИВП 247 Добавление ветви case в обработчик сообщения WM_COMMAND Включение показанного ниже фрагмента в обработчик сообщения WM_COMMAND гарантирует правильную обработку ввода в парное тексто- вое поле: case IDC_EDIT1: if ( (uCodeNotifу == EN_CHANGE ) && SCROLL_BuddyInitialized ) ProcessIntegerEditNotification( hDlg, &SCROLL_ProcessEdit, IDC_SCROLLBAR1, IDC_EDIT1, SCROLL_VALUE_MIN, SCROLL_NUMBER_INCREMENT S_PER_UNIT, &SCROLL_CurrentScrollPos, &SCROLL_CurrentScrollValue ) ; break ; Проверив код извещения uCodeNotify и флаг завершения инициализации SCROLLBuddylnitialized, обработчик решает, что введенные данные можно об- рабатывать. Если оба условия соблюдены, то детали отдаются на усмотрение вспомогательной функции ProcessIntegerEditNotification. Обработчик сообще- ния WM_HSCROLL (мы рассмотрим его в следующем разделе) мог поднять флаг SCROLL_ProcessEdit, чтобы подавить обработку сообщения, пришедшего в ре- зультате обновления парного поля после манипуляций с полосой прокрутки. ПРИМЕЧАНИЕ В GUIUtils.h имеются аналогичные функции для работы с целыми без знака, а также с числами двойной точности со знаком и без знака. При пользовании ими не забывайте об установке константы SCROLL NUMBERJNCREMENTS PER UNIT, чтобы точность обрабатывалась корректно. Добавление в диалоговую процедуру обработчика сообщения WM_HSCROLL Когда пользователь буксирует ползунок либо касается стилосом пустого мес- та или стрелок в полосе прокрутки, ее скрытая оконная процедура посылает сооб- щение WM_HSCROLL. Оно обрабатывается с помощью вспомогательной функ- ции ProcessScrollMessage из GUIUtils.h: void DlgOnHScroll(HWND hDlg, HWND hwndCtl, UINT Code, int Position) { HWND Scroll ; Scroll = GetDlgltem(hDlg,IDC_SCROLLBAR1) ; if ( Scroll == hwndCtl ) ProcessScrollMessage(hDlg, &SCROLL_ProcessEdit, Code, IDC_SCROLLBAR1, Position, &SCROLL_CurrentScrollPos, SCROLL_VALUE_MIN, SCROLL_NUMBER_INCREMENTS_PER_UNIT, &SCROLL_CurrentScrollvalue, IDC_EDIT1, SCR0LL_NUMBER_FRACTIONAL_DIGITS ) ;
248 ПН Применение элементов управления Передача этой функции флага SCROLL_ProcessEdit позволяет установить его значение так, чтобы запретить обработку сообщения WM_COMMAND, при- ходящего в результате обновления поля IDC_EDIT1. После выхода из Process- ScrollMessage текущее значение вводимой переменной отображается в поле ввода и сохраняется в переменной SCROLL_CurrentScrollValue. После того как все описанные изменения внесены, можно глобально заменить префикс SCROLL на что-нибудь более подходящее. Таким образом, включение дружелюбной полосы прокрутки оказывается механической процедурой, требую- щей минимального редактирования кода. Контроль прямого ввода в парное поле В файле GUIUtils.h среди прочих объявлена функция ProcessIntegerEdit- Notification. Напомним, что она вызывается в ответ на ввод символа в парное поле, void ProcessIntegerEditNotification( HWND hDlg, BOOL * ProcessEdit, int ScrollID, int BuddylD, double ValueMin, int NumberlncrementsPerUnit, int * Currentposition, double * Currentvalue ) { int NewValue ; if ( ‘ProcessEdit ) { ‘ProcessEdit = FALSE ; GetlntegerFromTextWindow(hDlg,BuddylD, SNewValue ) ; if ( NewValue >= ValueMin ) ‘Currentposition = (int) ((NewValue-ValueMin) * (double) NumberlncrementsPerUnit ) ; else ‘Currentposition = 0 ; ValidateScrollPosition(hDlg,ScrollID,Currentposition) ; ‘Currentvalue = ((double)‘Currentposition/(double)NumberlncrementsPerUnit) + ValueMin ; if ( ‘Currentvalue != NewValue ) SetlntegerlntoTextWindow(hDlg,BuddylD,(int)‘Currentvalue) ; UpdateScroll(hDlg,ScrollID, ‘CurrentPosition) ; ‘ProcessEdit = TRUE ; } } Сначала функция проверяет значение аргумента ProcessEdit. Если он равен FALSE, никакая обработка не выполняется. Такое бывает только в случае, когда этот флаг был установлен в FALSE обработчиком сообщения WM_HSCROLL пе- ред изменением значения в парном поле ввода в ответ на манипуляции с полосой прокрутки. Если же флаг равен TRUE, значит, пользователь сам ввел символ в парное поле. Функция получает текущее значение поля, вызывая вспомогательную функцию GetlntegerFromTextWindow, внутри которой проверяется, что это чис- ло, попадающее в допустимый диапазон. Полученное значение преобразуется
Резюме 1ИНП 249 в новую позицию ползунка. Для этого вызывается функция VaiidateScrollPosition, также принадлежащая GUIUtils, которая приводит значение к диапазону, задан- ному для полосы прокрутки. Если полученное значение было модифицировано процедурами контроля, то новое значение заносится в поле ввода. И наконец, новая позиция ползунка пере- дается функции UpdateScroll, которая устанавливает его в нужное положение. Резюме В этой главе мы познакомились со средствами построения пользовательского интерфейса для ввода данных. Такие встроенные элементы управления, как мет- ка, кнопка, список, комбинированный список, переключатель и флажок, позволя- ют организовывать ввод с минимальным участием клавиатуры или вообще без нее. Для быстрого ввода чисел мы разработали дружелюбную полосу прокрутки, специально предназначенную для работы на маленьком экране, а также библиоте- ку функций, упрощающих включение этого элемента в программу. Вот основные выводы из прочитанной главы: □ встроенные элементы управления легко применять в собственных про- граммах; □ для каждого встроенного элемента управления Windows регистрирует класс со скрытой оконной процедурой; □ когда пользователь взаимодействует со встроенным элементом управления, скрытая оконная процедура посылает сообщение родительскому окну; □ поскольку главное окно приложения является диалоговым, то поместить в него элементы управления можно с помощью визуального редактора; □ дружелюбная полоса прокрутки позволяет быстро вводить числовые дан- ные даже на маленьком экране Pocket PC; □ визуальный редактор позволяет очень быстро разместить в диалоговом окне элементы управления, входящие в состав дружелюбной полосы прокрутки; □ компонент GUIUtils дает возможность включить в программу дружелюбую полосу прокрутки путем механической процедуры, состоящей в основном из операций копирования и редактирования уже написанного кода. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание___________________________________________Папка______________________ Программа работы со встроенными элементами IntrinsicControlsProgram управления для настольного ПК Программа работы со встроенными элементами IntnnsicControlsProgramPPC управления для Pocket PC Программа работы с полосой прокрутки ScrollBarControlsProgram для настольного ПК Программа оаботы г полосой прокрутки для Pocket PC ScrollBarControlsProgramPPC
250 Применение элементов управления Инструкции по сборке и запуску Программа работы со встроенными элементами управления для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект IntrinsicControlsProgram.dsw в папке IntrinsicControls- Program. 3. Соберите программу. 4. Запустите программу. 5. Нажмите кнопку. Должно появиться сообщение о том, что была нажата кнопка. 6. Поработайте с другими элементами управления. При каждом взаимодейст- вии должно выдаваться сообщение. 7. Выберите пункт меню Quit. 8. Окно закроется, так как приложение завершило работу. Программа работы со встроенными элементами управления для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект IntrinsicControlsProgramPPC.vcw в папке IntrinsicControls- ProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку Му Device. 11. Запустите программу IntrinsicControlsProgram. 12. Коснитесь кнопки стилосом. Должно появиться сообщение о том, что была нажата кнопка. 13. Поработайте с другими элементами управления. При каждом взаимодейст- вии должно выдаваться сообщение. 14. Выберите пункт меню Quit. 15. Окно закроется, так как приложение завершило работу. Программа работы с полосой прокрутки для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект ScrollBarControlsProgram.dsw в папке ScrollBarControls- Program. 3. Соберите программу. 4. Запустите программу.
Примеры программ в Web IIIIMB1 251 5. Буксируйте ползунок. В парном поле ввода должны появляться новые зна- чения. Приращения будут довольно велики, но все значения будут нахо- диться в диапазоне, указанном в двух метках под полосой прокрутки. 6. Щелкните мышью в свободном месте полосы прокрутки. В парном поле ввода должно появиться новое значение. Приращение будет сравнительно невелико, и значение окажется в допустимом диапазоне. 7. Щелкните по стрелкам с обеих сторон полосы прокрутки. В парном поле ввода должно появиться новое значение. Приращение будет очень мало, и значение окажется в допустимом диапазоне. 8. Введите значение непосредственно в парное поле ввода. При вводе каждо- го символа значение в поле будет изменяться, а ползунок перемещаться в новое положение. 9. Выберите пункт меню Quit. 10. Окно закроется, так как приложение завершило работу. Программа работы с полосой прокрутки для Pocket PC . Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект ScrollBarControlsProgramPPC.vcw в папке ScrollBar- ControlsProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку Му Device. 11. Запустите программу ScrollBarControlsProgram. 12. Буксируйте ползунок. В парном поле ввода должны появляться новые значения. Приращения будут довольно велики, но все значения будут на- ходиться в диапазоне, указанном в двух метках под полосой прокрутки. 13. Коснитесь стилосом свободного места в полосе прокрутки. В парном поле ввода должно появиться новое значение. Приращение будет сравни- тельно невелико, и значение окажется в допустимом диапазоне. 14. Коснитесь стилосом стрелок с обеих сторон полосы прокрутки. В парном поле ввода должно появиться новое значение. Приращение будет очень мало, и значение окажется в допустимом диапазоне. 15. Выведите на экран клавиатуру. 16. Пользуясь стилосом, введите значение непосредственно в парное поле ввода. При вводе каждого символа значение в поле будет изменяться, а ползунок - перемещаться в новое положение. 17. Выберите пункт меню Quit. 18. Окно закроется, так как приложение завершило работу.
http: //all - ebooks. com Глава 9. Разработка сложного интерфейса пользователя Предлагая развитые средства для разработки приложений, платформа Pocket PC все же страдает существенным ограничением — малой площадью экрана. Типич- ный экран имеет разрешение 240 х 320 пикселей, а из-за рамки окна и полосы меню и эта площадь уменьшается примерно на 10%. Поэтому приходится задумы- ваться о создании изощренных пользовательских интерфейсов. Чтобы компенсировать малую площадь экрана, программист должен приме- нять два весьма специфических приема. Интерфейс разрабатывается в виде иерархии окон, по которым пользователь последовательно проходит, чтобы доб- раться до нужной функциональности. На нижнем уровне иерархии находится страница со вкладками, которые позволяют разместить разные наборы элементов управления в одной и той же области. В этой главе будет представлена программа рисования, в которой использу- ются оба способа проектирования интерфейса. Для этого мы воспользуемся не- сколькими специальными программными компонентами и разработаем функции для включения этих компонентов в приложение. Программа рисования со сложным интерфейсом пользователя В этой главе мы напишем усовершенствованную версию программы рисова- ния из главы 5. Пользователь сможет задать различные свойства инструмента рисо- вания, а затем нарисовать объект с помощью сконфигурированного инструмента. В начальный момент пользователь видит интерфейс, изображенный на рис. 9.1. Интерфейс в целом достаточно сложен, поэтому на первом шаге мы видим только верхний уровень иерархии. Здесь пользователю с помощью графических иконок предлагается выбрать один из двух режимов. ПРИМЕЧАНИЕ В действиях пользователя можно выделить крупные группы логически связанных функций. Эти группы нужно тщательно анализировать. Они должны включать шаги, которые типичный пользователь выполняет для решения конкретной задачи. Для разработки эффективного пользовательского интерфейса программист должен иметь навыки пользователя, хотя бы базовые. Например, если программа должна собирать отзывы о продукции и загружать их на сервер, то, по крайней
Программа рисования НИМ 253 мере, основные ее разработчики должны иметь хоть какой-то опыт анализа ка- чества продукта. Если автор программы понимает, в чем состоит работа пользова- теля, то может организовать интерфейс наиболее удобным для выполнения этой работы способом. К сожалению, многие программисты не хотят изучать работу пользователей. На настольных ПК это нежелание еще можно компенсировать за счет большого экрана и объема памяти. Но в случае Pocket PC глубокое понима- ние потребностей пользователей необходимо для выделения верхнего уровня ин- терфейса. Кнопка, рисуемая владельцем Метка с надписью Рис. 9.1. Сложный пользовательский интерфейс Компоненты интерфейса С точки зрения программирования каждая область показанного на рис. 9.1 интерфейса состоит из двух компонентов. Иконка открывает путь к одному из следующих уровней, пользователю достаточно коснуться ее стилосом. На рисун- ке видно, что этот элемент представляет собой кнопку, рисуемую владельцем. Та- кая кнопка может содержать произвольное растровое изображение, но нуждается в специальной обработке со стороны программы. К сожалению, иконки не слишком подходят в качестве средства передачи ин- формации. Поэтому во избежание путаницы каждая иконка сопровождается по- яснительной надписью, которая описывает, что находится на следующем уровне иерархического интерфейса. На рис. 9.1 представлены два раздела: Setup (Настройка) и Drawing (Рисова- ние). Обратите внимание, что иконка Setup находится в левой части, то есть пред- шествует Drawing. Это не случайно, ведь прежде чем воспользоваться инструмен- том рисования, его нужно настроить.
Q10MIIIII Разработка сложного интерфейса ПРИМЕЧАНИЕ Если дать себе труд подумать о том, как будет использоваться программа рисо- вания, то выбор и порядок расположения разделов на первом уровне интерфейса становятся очевидными. Это еще одно свидетельство в пользу того, что програм- мист должен перевоплощаться в пользователя, хотя бы начинающего. При касании иконки Setup происходит переход на следующий уровень интер- фейса, представленный на рис. 9.2. Это окно составляет самый нижний уровень интерфейса данного приложе- ния. Обратим внимание на новые особенности дизайна. Экран маленький, а пере- менных надо ввести много, поэтому интерфейс организован в виде нескольких вкладок. Это простой и элегантный способ преодолеть пространственное ограни- чение Pocket PC. U sei Interface Program - Setup Вкладки позволяют эффективно использовать место на маленьком экрана Return ► Shapes | Lines | Brushes | Geometry rr—;—:------------ ectangle Width Style На каждой вкладке расположены параметры из одной категории. Они представлены различными элементами управления Рис. 9.2. Организация ввода параметров в процессе настройки Каждая вкладка представляет одну категорию входных параметров, логиче- ски связанных между собой. ПРИМЕЧАНИЕ И здесь ощущается необходимость в навыках пользователя. Это позволит сгруп- пировать параметры - в данном случае для настройки инструментов рисования - наиболее естественным образом.
Программа рисования 1И11ПН Каждая вкладка снабжена меткой, описывающей, какие входные параметры на ней находятся. СОВЕТ Из-за ограниченности экрана метка должна состоять из одного слова. На самой вкладке мы видим встроенные элементы управления, с помощью которых задаются входные параметры. Каждый элемент сопровождается меткой, описывающей его назначение. Если параметр может принимать значения из диск- ретного множества, то лучше представлять его комбинированным списком. Для ввода числовых значений мы пользуемся дружелюбной полосой прокрутки, раз- работанной в главе 8. Задав все параметры из категории Shapes, пользователь переходит на следую- щую вкладку Lines, коснувшись стилосом ее заголовка. Перед тем как перейти на другую вкладку, приложение сохраняет введенные значения в глобальных пере- менных. Обычно для хранения глобальных данных применяется компонент DataMgr, описанный в главе 3. Задав все параметры (или оставив значения, под- разумеваемые по умолчанию), пользователь касается пункта Return в главном меню и возвращается на верхний уровень интерфейса. Затем он нажимает кнопку Drawing, и программа переходит на страницу ри- сования, где с помощью стилоса пользователь рисует фигуру методом эластично- го контура. Внешний вид этой части интерфейса показан на рис. 9.3. User Interface Program - Drawing * В заголовке окна обозначена — та область интерфейса, Return с которой мы работаем Для выхода из этой области выбрать пункт Return Рисование производится с учетом заданных параметров Рис. 9.3. Выполнение операции рисования
256 Illi Разработка сложного интерфейса Ha этом рисунке мы видим, как используются параметры, заданные на рис. 9.2. Там была выбрана фигура «прямоугольник» и ширина линии 5. Именно эта фигу- ра и была нарисована, причем границы проведены линией толщиной 5 пикселей. На рис. 9.3 показан еще один важный аспект дружелюбного интерфейса. По- скольку пользователь вынужден путешествовать по сложной иерархии окон, про- грамма сообщает, в какой части интерфейса он сейчас находится. Эта информа- ция помещена в заголовок окна, где сразу бросается в глаза. ПРИМЕЧАНИЕ Размещая информацию о месте нахождения в иерархическом интерфейсе в заго- ловке окна, мы не расходуем ценную площадь клиентской области. На рис. 9.3 заголовок содержит путь по иерархии «User Interface Program — Drawing». Он идет от корневого элемента, каковым является сама программа, до текущего окна и включает все промежуточные стадии. В более сложных иерархи- ях название корневого элемента можно опускать, поскольку, как видно из рисун- ка, оно занимает слишком много места. Каждый промежуточный элемент лучше обозначать одним словом, все из тех же соображений экономии места. На рис. 9.4 представлен полный иерархический интерфейс рассматриваемого простого приложения. [Вкладки + элементы [Вкладки + элементы [Вкладки + элементы управления] управления] управления] Рис. 9.4. Иерархический пользовательский интерфейс Хотя эта программа обладает не слишком богатыми возможностями, ее интер- фейс состоит из трех уровней. Ясно, что в более сложных программах число уров- ней возрастет.
Применение графических кнопок 1И11ПН 257 ПРИМЕЧАНИЕ 'i ;ninjnftHeji8ij,iiift)ilwi>wiiH4HWiHffliw'i>qiiinoiiMtt^>№Hi*«ia»Hwiwr».n.№WM>iJjo.ijjjp;H^jtWr,ir.if . ч^.7-,гл‘>:гла^|№Г7Л'-^ --х>^ «'«mihh Разрабатывая иерархический интерфейс, программист должен искать компромисс между числом уровней и наличием места на экране. Если уровней слишком много, то для выполнения даже самых простых действий придется долго добираться до нужного окна. А если их слишком мало, то не удастся разместить все элементы на маленьком экране. Отыскать баланс можно, только обладая знаниями в предмет- ной области. Нужно представлять, как пользователь подходит к решению стоящей перед ним задачи и как программа могла бы ему в этом помочь. Помимо иерархии, на рис. 9.4 есть еще требования к реализации. Под разде- лом Setup отмечено, что реализовывать его надо в виде страницы со вкладками. Для каждой категории предусматривается отдельная вкладка, на которой будут находиться соответствующие параметры. Применение графических кнопок для организации иерархий Для реализации графических кнопок, дающих доступ к разным разделам пользовательского интерфейса, можно применить элементы управления, рисуе- мые владельцем. Правда, для этого потребуется затратить некоторое время на на- писание кода. А раз так, то имеет прямой смысл завести повторно используемый компонент BitmapButtonMgr, который будет инкапсулировать все детали реали- зации подобных элементов. Шаги, необхимыедля включения в программу графических кнопок После добавления в проект файлов BitmapButtonMgr.h и BitmapButtonMgr.c для включения в пользовательский интерфейс графических кнопок нужно вы- полнить следующие действия. 1. С помощью редактора ресурсов добавить ресурс «растровое изображение». 2. С помощью редактора диалогов поместить в форму кнопку и метку, кото- рые в совокупности будут представлять графическую кнопку. 3. В диалоговую процедуру включить заголовочный файл BitmapButtonMgr.h. 4. Объявить переменную типа BitmapButtonType. 5. В обработчике сообщения WM_INITDIALOG создать объект из ресурса, в котором хранится растровое изображение. 6. В обработчике сообщения WM_COMMAND уничтожить этот объект в ветви IDOK. 7. Добавить обработчик сообщения WM_DRAWITEM, который будет рисо- вать изображение на поверхности кнопки. Все это легко проделать, скопировав небольшие участки кода из программ- примеров
258 Illi Разработка сложного интерфейса Пример добавления графических кнопок В программе UserlnterfaceProgram из этой главы используются только две гра- фические кнопки. Здесь мы покажем, как добавляется кнопка Setup. Добавление растрового изображения с помощью редактора ресурсов Редактор ресурсов, входящий в состав Visual Studio, позволяет среди прочего добавлять в качестве ресурсов растровые изображения. В главе 2 было описано, как включить ресурс, состоящий из изображения размером 32 х 32 пикселя, а на рис. 9.5 показано, как выглядит окно редактора ресурсов после добавления двух изображений. В меню Visual Studio есть специальные пункты для включения изображений в состав ресурсов. Если имеется уже готовая картинка, то ее можно импортиро- вать в проект. Главное, чтобы ее размер был равен 32 х 32. Цветность каждого пик- селя зависит от глубины цвета на целевом КПК Pocket PC. Разные модели под- держивают разное число цветов. Импортированную картинку можно изменить с помощью редактора изобра- жений. Дважды щелкните кнопкой мыши по ресурсу, соответствующему изобра- жению, в окне редактора ресурсов. В ответ Visual Studio запустит графический редактор. ПРИМЕЧАНИЕ Надо очень тщательно подходить к выбору картинки, описывающей назначение той или иной части интерфейса. И в этом отношении знакомство с нюансами работы пользователя было бы очень полезно. 1<3пг DhjFoim.rc ...--- -.L... ВИ 13 Папка Bitmap, содержащая графические ресурсы Ё-МдЗ Bitmap 4--------- pffi IDB.BITMAP1 IDB_BITMAP2 с bQ Dialog Menu Рис. 9.5. Окно редактора ресурсов после добавления двух растровых изображений Графические ресурсы с присвоенными им символическими именами
Применение графических кнопок 1И11ПН 259 На рис. 9.6 показана картинка, выбранная для графической кнопки Setup. В ней использовано всего 16 цветов, так что она будет прекрасно отображаться на любом КПК. ^DlgForm гс - IDB„BITMAP1 (Bitmap) ИИЕЗ Каждая клеточка соответствует одному пикселю. Для манипулирования пикселями имеется ряд инструментов Рис. 9.6. В редакторе ресурсов представлены пиксели изображения Чем больше цветов задействовано в изображении, тем детальнее получается картинка. Линии выглядят более четкими и плавными. Цвета ярче. Но это накла- дывает и дополнительные требования к устройству, на котором такая картинка будет отображаться. Поэтому необходимо соблюдать баланс между возможностя- ми целевого КПК и качеством изображения. Добавление кнопки и метки в форму диалога Как показано на рис. 9.1, графическая кнопка включает два встроенных эле- мента управления: кнопку, рисуемую владельцем, и метку. Разработчик просто перетаскивает оба этих элемента с панели инструментов в форму диалога. На рис. 9.7 показан результат этой операции для программы UserlnterfaceProgram. Каждая кнопка имеет размер 16 х 16 (в стандартных единицах измерения ди- алога). Единица измерения диалога в Windows - это мера длины, не зависящая от характеристики физического экрана. Метки следует разместить под кнопками и выровнять по центру. Они должны ясно описывать назначение соответствующего раздела интерфейса. С учетом малых размеров экрана Pocket PC лучше, чтобы текст метки состоял всего из одного короткого слова. Кнопки, показанные на рис. 9.7, обладают некоторыми особенностями. Двой- ной щелчок по кнопке в Embedded Visual Studio открывает окно ее свойств Push Button Properties. Перейдите в нем на вкладку Styles (рис. 9.8). Как видите, отмечены флажки, соответствующие всего двум стилям. Стиль Owner Draw (Рисуется владельцем) заставляет Windows посылать приложению сообщение WM_DRAWITEM. Это означает, что приложение принимает на себя ответственности т> чание на поверхности кнопки. Стиль Flat (Плоская) по-
260 III! Разработка сложного интерфейса давляет рисование трехмерной рамки, которая для графической кнопки была бы излишней. Убедитесь, что никакие другие флажки стилей не отмечены, в противном случае при рисовании картинки на поверхности кнопки возможны нежелательные эффекты. 'User Interlace Program — Форма диалога или шаблон uttor Setup Drawing < Кнопка, рисуемая владельцем Пояснительная метка ? s http: //all - ebooks. com Рис. 9.7. Форма диалога со встроенными элементами управления Элемент рисуется владельцем Вкладка расширенных стилей Pu Ji Button Propnrti??__________________________________________________________D T General ; Styles j| Extended Styles J C Default button Г Multiline —Owner draw Г” Notify П Igon |“j Bitmap pRat Horijontal alignment: | Default Vertical alignment: | Default 2 Плоская рамка Рис. 9.8. Свойства кнопки, рисуемой владельцем Включение компонента BitmapButtonMgr #include "BitmapButtonMgr-h" Включение этого заголовочного файла делает все методы, входящие в состав компонента BitmapButtonMgr, видимыми диалоговой процедуре.
261 Применение графических кнопок Н11НИМИ1 Объявление переменной типа BitmapButtonType Компонент BitmapButtonMgr предоставляет абстрактный тип данных. В про- грамме может быть несколько экземпляров такого типа. Для каждой графической кнопки следует объявить отдельную переменную типа BitmapButtonType. Давай- те переменным имена, описывающие назначение кнопок. Если кнопка управляет доступом к некоторому разделу интерфейса, включите в имя переменной пре- фикс, содержащий название раздела, и строку BitmapButton. Тогда будут сразу понятны и назначение, и тип переменной. Эти переменные необязательно делать статическими. Спецификатор static ог- раничил бы видимость переменной только диалоговой процедурой. Основной смысл такого объявления - обеспечить доступ к переменной лишь со стороны об- работчиков сообщений. Для этого нужно было бы объявить переменную в файле DlgProc.c, но вне всех обработчиков сообщений. Создание объекта ресурса растрового изображения SetupBitmapButton = CreateBitmapButton(hDlg, IDB_BITMAP1) ; Так как графической кнопкой управляет приложение, то создать соответствую- щий ей объект нужно в обработчике сообщения WM_INITDIALOG. Для этого компонент BitmapButtonMgr предоставляет функцию CreateBitmapButton. В качестве первого аргумента передается описатель окна, в котором создается кнопка, а в качестве второго - идентификатор ресурса, содержащего изображе- ние, рисуемое на поверхности кнопки. Уничтожение объекта в ветви IDOK DestroyBitmapButton(SetupBitmapButton) ; Перед завершением программа явно уничтожает графическую кнопку. Для этого предназначена функция DestroyBitmapButton из компонента BitmapButtonMgr. Ее единственный аргумент - объект, возвращенный функцией CreateBitmapButton. Добавление обработчика сообщения WM_DRAWITEM void DlgOnDrawItem(HWND hDlg, const DRAWITEMSTRUCT * Drawitem) { UINT ControlID ; ControlID = DrawItem->CtlID ; if ( ControlID == IDC_BUTTON1 ) { DisplayBitmapButton(SetupBitmapButton,DrawItem->hDC,32,32) ; ) ) Поскольку для графической кнопки был задан стиль Owner Drawn, то всякий раз, как Windows должна нарисовать эту кнопку, она посылает сообщение WM_DRAWITEM. Его основной параметр - указатель на структуру DRAWI- TEMSTRUCT, в которой находится информация, необходимая для рисования кнопки
262 Illi Разработка сложного интерфейса В частности, в этой структуре есть поле CtllD, содержащее целочисленный идентификатор встроенного элемента управления, который необходимо нарисо- вать. Для наглядности мы сначала копируем этот идентификатор в локальную переменную ControlID. Поскольку этот обработчик вызывается для всех графи- ческих кнопок, то в предложении if управление передается на участок кода, рису- ющий конкретную кнопку. Чтобы нарисовать изображение, обработчик вызывает функцию Display- BitmapButton, принадлежащую компоненту BitmapButtonMgr, передавая ей кон- текст устройства, хранящийся в поле hDC структуры Drawitem, а также ширину и высоту кнопки. Вот и все, что необходимо для включения графической кнопки в приложение. Нетривиальные детали программирования инкапсулированы в компоненте BitmapButtonMgr. Обзор реализации BitmapButtonMgr Компонент BitmapButtonMgr предоставляет абстрактный тип данных, кото- рым можно пользоваться в приложении. Разрешается создавать несколько экзем- пляров типа BitmapButtonType. Объявление типа BitmapButtonType приведено ниже: typedef struct { HDC MemoryDC ; } BitmapButtonRecordType ; С каждой кнопкой ассоциирована переменная типа HDC. Это тот контекст устройства в памяти, в котором хранится растровое изображение для данной кнопки. Хотя сейчас в структуре всего одно поле, позже ее можно будет расши- рить, если появится необходимость хранить дополнительные данные для графи- ческой кнопки. Далее мы объявляем абстрактный тип данных как указатель на эту структуру. Тем самым скрывается сам факт наличия указателя. Да, первым аргументом лю- бой функции, оперирующей графической кнопкой, передается этот замаскиро- ванный указатель. Но внешней программе он представляется как абстрактный объект, что несколько повышает надежность ее работы. В листинге ниже показано, как функция CreateBitmapButton создает объекты типа BitmapButtonType: BitmapButtonType CreateBitmapButton(HWND Window, int BitmapID) { // Объявления переменных опущены, полный код см. на сайте BitmapButton = (BitmapButtonType) malloc( sizeof(BitmapButtonRecordType) ) ; Bitmap = LoadBitmap(Instance,MAKEINTRESOURCE(BitmapID)) ; DeviceContext = GetDC(Window) ; MemoryDC = CreateCompatibleDC(DeviceContext) ; Selectobject(MemoryDC,Bitmap) ;
263 Применение вкладок ReleaseDC(Window,DeviceContext) ; BitmapButton->MemoryDC = MemoryDC ; return BitmapButton ; } Прежде всего с помощью функции malloc выделяется память для самой струк- туры. Затем от функции LoadBitmap мы получаем описатель ресурса, как описано в главе 2. Далее создается контекст устройства в памяти и к нему привязывается загру- женное изображение. Этот шаг необходим, так как для копирования изображения на экран нужен контекст устройства (см. главу 6). В обработчике сообщения WM_DRAWITEM вызывается функция Display- BitmapButton, которая и занимается выводом изображения. Ниже приведен соот- ветствующий фрагмент: void DisplayBitmapButton(BitmapButtonType BitmapButton, HDC ButtonDC, int Width, int Height) ( BitBlt(ButtonDC,0,0,Width,Height,BitmapButton->MemoryDC,0,0,SRCCOPY) ; } Первым аргументом этой функции передается объект описанного выше типа BitmapButtonType. Контекст устройства ButtonDC представляет поверхность, на которой рисуется кнопка. Для рисования кнопки вызывается функция Win32 API BitBlt, которая про- сто копирует изображение из контекста MemoryDC в контекст, с которым ассоци- ирована кнопка. Функция BitBlt выполняет копирование пиксель в пиксель. Для копирования с масштабированием следовало бы вызвать функцию StretchBlt, описанную в главе 6. Компонент BitmapButtonMgr предоставляет средства для удобного доступа к разделам пользовательского интерфейса. А раздел состоит из вкладок, позво- ляющих разместить множество элементов управления в одной и той же области экрана. Применение вкладок для организации категорий Категория - это группа логически связанных входных параметров. Для каж- дой категории предусмотрена отдельная вкладка. Вкладки - очень естественный способ ввода данных, так как большинство пользователей знакомы с каталожны- ми карточками. Для реализации вкладок необходимы диалоги двух типов. Родительский диа- лог служит контейнером, а кроме того, для каждой вкладки нужен еще диалог, представляющий страницу, на которой она расположена. Так, для окна с тремя вкладками понадобится четыре диалога, а следовательно, и четыре диалоговые процедуры. Диалоговая процедура для родительского диалога отвечает за построе-
264 1111 Разработка сложного интерфейса ние отдельных вкладок и переходы с одной вкладки на другую. Кроме того, для каждой вкладки нужна еще процедура, которая будет реагировать на взаимодейст- вия пользователя с элементами управления, размещенными на этой вкладке. В пап- ке Reusable Components на сайте этой книги (http.//www.osborne.com) есть шаб- лоны процедур для родительского диалога и отдельных вкладок. Механизм создания вкладок для элемента управления Tab Control, описан- ный в документации по Win32 API, громоздкий и трудоемкий. В этой главе мы познакомимся с компонентом TabPageMgr, который инкапсулирует все детали. С его помощью мы сумеем легко и быстро создать вкладки в своем приложении. Шаги, необходимые для работы с компонентом TabPageMgr и шаблонами вкладок Прежде всего необходимо включить в проект сам компонент TabPageMgr, а затем выполнить следующие действия. 1. Создать диалоговые процедуры по имеющимся шаблонам. 2. В родительский диалог поместить элемент Tab Control. 3. Для каждой вкладки создать отдельный диалог без рамки. 4. Изменить диалоговую процедуру в родительском диалоге, так чтобы она создавала вкладки и управляла переходами. 5. Изменить диалоговые процедуры вкладок, добавив обработчики сообще- ний от размещенных на них элементов управления. 6. Включить в компонент DataMgr функции PutTabPage и GetTabPage. 7. Сконфигурировать проект для применения вкладок. Все эти шаги несложны. Пройдя их один раз, вы впоследствии будете проде- лывать это механически, и на добавление страниц со вкладками в свое приложе- ние будет уходить совсем немного времени. Пример включения компонента TabPageMgr В этом разделе мы продемонстрируем шаги, необходимые для включения вкладок в программу UserInterfaceProgram. При этом рассматриваются только те изменения, которые следует выполнить на верхнем уровне приложения. Весь ме- ханизм работы со вкладками уже включен в шаблон и не нуждается в модифика- ции, поэтому говорить о нем мы не будем. Создание диалоговых процедур для шаблонов диалогов На этом шаге создаются диалоговые процедуры для родительского диалога и отдельных вкладок. 1. Скопируйте в папку проекта файл \ReusableComponents\TabParentDlgProc.c. Переименуйте этот файл во что-нибудь более подходящее для вашего кон- кретного приложения, например SettingDlgProc.c. 2. Для каждой вкладки скопируйте в папку проекта файл \ReusableCompo- nents\TabDlgProc.c. Переименуйте файл, так чтобы его имя отражало на-
265 Применение вкладок значение вкладки, например LinesDlgProc.c, ShapeDlgProc.c и Brush- DlgProc.c. 3. Включите эти файлы в проект. В результате в проект будут включены все необходимые диалоговые процеду- ры. Но в них еще потребуется внести некоторые изменения. Попытка откомпили- ровать проект на этой стадии приведет ко множеству ошибок Создание элемента управления Tab Control в родительском диалоге В редакторе ресурсов перетащите в диалог иконку с изображением миниатюр- ной страницы со вкладками, а затем растяните ее до нужного размера. Для этого потяните за правый нижний угол, установив размер равным 148 х 136 диалоговых единиц измерения. Расположите элемент Tab Control по центру. Результат дол- жен выглядеть, как показано на рис. 9.9. При указанном размере элемент займет почти всю площадь окна, так что у разработчика будет достаточно места для размещения всех необходимых эле- ментов управления на вкладках. jDIgFoim.rc IDD_OIALOG3 (Dtnlagl МПЙИ I •! ... I i . U p.........-.............. ’"'U'.......... *...*....... - iUse> fiiteifoce Ршдмв* - Setup 2 : ТаЫ [ ТаЬ2 I ТаЬЗ I Tab4 I Tab 5 I ' Элемент Tab Control занимает почти всю площадь диалогового окна Рис. 9.9. Родительский диалог с элементом Tab Control Создание диалогов без рамки для каждой вкладки Для каждой вкладки необходим отдельный диалог, служащий контейнером для элементов управления. У таких диалогов не должно быть рамки, тогда они естественно сольются с объемлющей страницей. Если оставить рамку, то резуль- тат будет отталкивающи
266 Illi Разработка сложного интерфейса Ha рис. 9.10 показан диалог для вкладки Lines без рамки. Элементы управления внутри этого диалога можно использовать для ввода параметров. По мере ввода диалоговая процедура передает значения параметров компоненту DataMgr для хранения и записывает их в конфигурационный файл, находящийся в перезаписываемой области памяти Pocket PC. Свойства вкладки задаются в диалоговом окне, показанном на рис. 9.11. Чтобы открыть это окно, дважды щелкните по форме в редакторе диалогов. На вкладке Styles перечислены стили окна диалога. В раскрывающемся списке Style выберите строку Child (дочернее окно), а в раскрывающемся списке Border (Рамка) - строку None (Без рамки). Сбросьте все остальные флажки во избежа- ние нежелательных эффектов во время работы. Модификация родительской диалоговой процедуры После создания всех визуальных элементов на вкладке нужно внести ряд из- менений в код. Это сводится к модификации уже имеющихся шаблонов диалого- вых процедур. BOOL CALLBACK ChildYDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) ; Шаблон родительской диалоговой процедуры состоит всего из одного пред- ложения, в котором объявляется дочерняя диалоговая процедура общего вида. Скопируйте это предложение столько раз, сколько вкладок будет на странице. Затем измените имя, так чтобы отразить назначение каждой диалоговой процеду- ры. Задавайте те же префиксы, что при переименовании файлов, содержащих ди- алоговые процедуры вкладок. Например, диалоговую процедуру, находящуюся в файле LinesDlgProc.c, следует назвать LinesDlgProc.
Применение вкладок 267 Стиль дочернего окна Dialog Properties -W Т General , Styles j| Mae Styles | Extended Styles | К |< |» Style___________________ Г Jitieba Г Chfi siblings I (child f“ 'yjeir'rn- Г Clip £hildten 1 Bader Г" Mi'iimcet Г” Horizontal scrol 3 Г Г Verted scroll J Г I । !........................................ । Стиль окна без рамки Все остальные флажки сброшены Рис. 9.11. Свойства диалогового окна вкладки BOOL CALLBACK ParentXDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam ) { switch (message) { // Другие обработчик для краткости опущены HANDLE_DLG_MSG( hDlg, WM_NOTIFY, ParentXDlgOnNotifу ) ; } return FALSE ; } В шаблоне родительской диалоговой процедуры префикс ParentX стоит всю- ду, где должно быть специфичное для приложения имя. С помощью глобальной замены подставьте вместо ParentX осмысленное имя. Как и в случае имен диало- говых процедур, задавайте то же имя, что для файла, содержащего текст процеду- ры. В нашем примере для процедуры в файле SettingsDlgProc.c следует вместо ParentX подставить Settings. BOOL OnlnitSettingsDialog ( HWND hDlg, HWND hwndFocus, long UnitParam ) { // Часть кода для краткости опущена TabPage = CreateTabPage(3,IDC_TAB1) ; PutTabPage(TabPage) ; // AddTab(TabPage, hDlg, IDD_DIALOGY,______TEXT(«Velocities»)) ; return TRUE; } В обработчике сообщения WM_INITDIALOG в шаблоне родительской диа- логовой процедуры функции из компонента TabPageMgr вызываются для созда- ния объекта TabPage, представляющего набор вкладок. В нем хранится вся ин- формация об э’| 1 «' управления Tab Control и его вкладках. Чтобы создать
268 НИ Разработка сложного интерфейса страницу со вкладками для своего приложения, разработчик должен модифици- ровать этот код. Прежде всего нужно изменить аргументы, передаваемые функции Create- TabPage. Ее первый аргумент - число вкладок. При добавлении в диалог элемента Tab Control ему был присвоен числовой идентификатор, он-то и передается в ка- честве второго аргумента. Создав объект Tab Page, обработчик включает в него вкладки с помощью фун- кции AddTab. Таких вызовов должно быть столько, сколько имеется вкладок. Скопировав эти предложения, замените IDD_DIALOGY идентификатором со- зданного ранее диалога для вкладки. Последний аргумент функции AddTab - текст, который должен отображаться в заголовке вкладки. Напомним, что макрос __TEXT обеспечивает переносимость программы с настольного ПК на Pocket PC. VOID WINAPI ParentXOnSelChanged(HWND hwndDlg) { // Часть кода опущена Selection = TabCtrl_GetCurSel(Tabwindow); switch( Selection ) { case 0: // OnTabSwitch(TabPage, 0, hwndDlg, IDD_DIALOGY,ChildYDlgProc) ; break ; } } Когда пользователь касается стилосом заголовка вкладки, скрытая окон- ная процедура элемента Tab Control посылает родительскому окну сообщение WM_NOTIFY. Его основной параметр - указатель на структуру типа NMHDR, заголовок извещения. Одним из полей этой структуры является код извещения. Если он равен TCN_SELCHANGE, то обработчик сообщения вызывает функцию OnSelChanged. Эта логика уже присутствует в шаблоне родительской диалоговой процедуры и не требует никаких модификаций. С помощью макроса TabCtrl_GetCurSel, определенного в файле windowsx.h, обработчик OnSelChanged определяет индекс вкладки, на которую хочет перейти пользователь, а в предложении switch управление передается нужной функции. Программист должен добавить по одной ветви case для каждой вкладки. Парамет- ры, передаваемые функции OnTabSwitch в каждой ветви, зависят от индекса. В шаблоне родительской диалоговой процедуры уже есть снабженный ком- ментариями пример вызова этой функции. Замените IDD_DIALOGY идентифи- катором диалога для соответствующей вкладки. В качестве последнего аргумента укажите вместо ChildYDlgProc имя диалоговой процедуры для той же вкладки. Модификация диалоговых процедур вкладок После изменения родительской диалоговой процедуры нужно еще модифи- цировать диалоговые процедуры каждой вкладки. Ниже приведен фрагмент шаб- лона такой процедуры: BOOL CALLBACK TabXDlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM IParam )
Применение вкладок HIIIHH 269 switch (message) { HANDLE_DLG_MSG( hDlg, WM_INITDIALOG, OnlnitTabXDialog ) ; // Часть кода опущена } return FALSE ; } В шаблоне процедуры встречается строка ТаЬХ, вместо которой следует под- ставить осмысленное значение. В родительской диалоговой процедуре строка ChildY в объявлениях заменялась словом, описывающим категорию параметров на вкладке. Это же слово нужно подставить и вместо ТаЬХ в тексте диалоговой процедуры для той же вкладки. Например, в родительской процедуре вместо ChildY мы подставили Lines, так что получилось имя LinesDlgProc. Поэтому в файле LinesDlgProc.c нужно тоже заменить ТаЬХ на Lines. После этого файл LinesDlgProc.c будет корректно компилироваться. Код, уже включенный в диалоговую процедуру вкладки, реализует механизм перехода с одной вкладки на другую. Программисту остается добавить в процеду- ру каждой вкладки код для создания, инициализации, обслуживания и уничтоже- ния элементов управления. Добавление функций PutTabPage и GetTabPage в компонент DataMgr Весь механизм перехода с одной вкладки на другую уже инкапсулирован в компоненте TabPageMgr. Но для работы ему необходим доступ к объекту типа TabPageType, который был создан функцией CreateTabPage в родительской диа- логовой процедуре. Это глобальная переменная, и хранится она как обычно в цент- ральном репозитарии, управляемом компонентом DataMgr. ♦include "TabPageMgr.h” static TabPageType CurrentTabPage ; void PutTabPage(TabPageType TabPage) { CurrentTabPage = TabPage ; } В этом фрагменте реализовано сохранение объекта типа TabPageType. Мы включили файл TabPageMgr.h, чтобы предоставить доступ к определению типа, указали спецификатор static, чтобы разместить объект в статической памяти, и реализовали методы доступа к объекту. Этот код можно скопировать в файл DataMgr.c в проекте любого приложения без какой бы то ни было модификации. И не забыть при этом поместить в DataMgr.h объявления методов доступа. Конфигурирование проекта для применения вкладок И напоследок нужно еще позаботиться о том, чтобы программа правильно компоновалась. Элемент Tab Control входит в библиотеку стандартных элемен- тов управления, котора” 'е компонуется с программой автоматически.
270 [Illi Разработка сложного интерфейса Для добавления этой библиотеки в проект необходимо выполнить следующие действия. 1. Выберите пункт Project (Проект) главного меню Embedded Visual Studio. 2. Выберите из ниспадающего меню пункт Setup (Настройка). 3. Перейдите на вкладку Link (Компоновка) в диалоговом окне Project Settings. 4. Щелкните по полю ввода Object / Library (Объектные файлы / Библио- теки). 5. Введите в это поле строку comctl32.1ib и пробел. 6. Нажмите кнопку ОК. Теперь программа будет корректно компилироваться и компоноваться. Пере- ходы между вкладками будут обрабатываться правильно. Эффективный подход к разработке требует сначала проверить работоспособ- ность именно этих модификаций. После того как отображение вкладок и перехо- ды между ними будут отлажены, можно сосредоточиться на реализации функ- циональности элементов управления на отдельных вкладках. Вообще, над разными вкладками могут независимо работать разные программисты. Закончив отладку, они возвращают код в общий проект, тем самым уменьшая общее время разработки. Обзор реализации шаблонов страниц со вкладками Компонент TabPageMgr инкапсулирует структуру данных, в которой хранит- ся вся информация, относящаяся к элементу Tab Control и самим вкладкам. Ниже приведено объявление этой структуры и типа TabPageType. typedef struct ( int RECT DLGTEMPLATE ** int int HWND } TabPageRecordType ; TabPagelD ; DisplayRect; TabTemplates ; NumberTabs ; MaxNumberTabs ; CurrentTab ; typedef TabPageRecordType * TabPageType ; Как и обсуждавшийся выше компонент BitmapButtonMgr, компонент TabPageMgr реализует абстрактный тип данных. Такой подход необходим, пото- му что приложению в любой момент времени может понадобиться более одного объекта данного типа. Как и для всех абстрактных типов данных в этой книге, объявление состоит из двух частей. Сначала объявляется сама структура данных, а затем typedef, кото- рый скрывает тот факт, что абстрактный тип является указателем. TabPageType CreateTabPage(int MaxNumberTabs, int TabPagelD) { TabPageType TabPage ; TabPage = (TabPageType) malloc(sizeof(TabPageRecordType)) ;
Заключительные замечания 1И11ПН 271 TabPage->MaxNumberTabs = MaxNumberTabs ; TabPage->NumberTabs = 0 ; TabPage->TabPageID = TabPageXD ; TabPage->TabTemplates = (DLGTEMPLATE **) malloc(MaxNumberTabs * sizeof(DLGTEMPLATE *)) ; TabPage->CurrentTab = NULL ; return TabPage ; } В структуре TabPageRecordType хранится таблица с данными обо всех вклад- ках в элементе Tab Control. В ней каждая вкладка представлена указателем на шаблон ее диалога в памяти. Обратите внимание, что в поле TabTemplates записы- вается указатель типа (DLGTEMPLATE **). Это означает, что речь идет о масси- ве указателей на шаблоны диалогов (структуры типа DLGTEMPLATE). void AddTab(TabPageType TabPage, HWND Dialog, int TablD, LPTSTR TabTitle) ( TCITEM Item; /I Часть кода опущена Item.mask = TCIF_TEXT | TCIF_IMAGE; Item.pszText = TabTitle ; TabCtrl_InsertItem(TabWindow, TabPage->NumberTabs, SItem); } Выше показан фрагмент функции AddTab. Для добавления новой вкладки сначала объявляется переменная типа TCITEM. Затем в поле mask структуры TCITEM поднимаются некоторые флаги, а в поле pszText заносится заголовок вкладки. И в заключение новая вкладка добавляется к элементу Tab Control путем обращения к макросу TabCtrl_InsertItem, который находится в файле windowsx.h. Интерес представляют аргументы этого макроса. Второй аргумент - индекс вкладки. Он равен величине TabPage->NumberTabs, которая вычисляется в той части кода, которую мы опустили. А последний аргумент - указатель на структу- ру TCITEM, в которой хранятся флаги и заголовок вкладки. Заключительные замечания для разработчиков В этой главе мы несколько раз подчеркивали, как важно знакомство с работой пользователя для разработки интуитивно понятного интерфейса. Многие про- граммисты не хотят прилагать к этому усилий, предпочитая сидеть в кабинете и писать код в вакууме. В результате получаются недружелюбные, интуитивно не- понятные интерфейсы, да и качество кода страдает. Если разработчик будет пря- тать голову в песок, то вполне может случиться, что его творение будет работать медленно, а возможно, даже не поместится в имеющуюся память. И последнее. Представьте, каким интересным может оказаться знакомство с работой другого человека. Ведь тем самым вы даром получаете знания, которые не каждому доступны
272 lllll Разработка сложного интерфейса Резюме В этой главе описаны два компонента - BitmapButtonMgr и TabPageMgr, при- меняемые для построения пользовательских интерфейсов программ, работающих на платформе Pocket PC. Вот что следует запомнить: □ из-за ограниченности экрана приходится разрабатывать иерархические интерфейсы; □ чтобы интерфейс оказался удобным, разработчик должен понимать, как с его помощью пользователь станет выполнять свою работу; □ на верхнем уровне иерархии располагаются элементы, открывающие до- ступ к крупным разделам интерфейса; □ уровни, расположенные ниже, соответствуют более узким категориям ло- гически связанных входных и выходных параметров; □ для описания разделов интерфейса применяются рисуемые владельцем кнопки в сочетании с метками; □ для визуального представления категорий параметров используются стра- ицы со вкладками; □ вкладки позволяют отобразить различную информацию в одной и той же области экрана и тем самым преодолеть ограничения, связанные с размером. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка Пользовательский интерфейс для настольного ПК UserlnterfaceProgram Пользовательский интерфейс для Pocket PC UserlnterfaceProgramPPC Инструкции по сборке и запуску Пользовательский интерфейс для настольного ПК 1. Запустите Visual C++6.0. 2. Откройте проект UserlnterfaceProgram.dsw в папке UserlnterfaceProgram. 3. Соберите программу. 4. Запустите программу. 5. Щелкните по графической кнопке Settings. Вы окажетесь в диалоговом окне Settings и увидите набор вкладок, причем открыта будет вкладка Lines. 6. Задайте какие-нибудь параметры на вкладке Lines. 7. Зайдите на другие вкладки и с помощью находящихся на них элементов управления задайте значения различных параметров рисования. При щелч- ке по заголовку вкладки программа должна скрыть текущую страницу и показать следующую.
Примеры программ в Web 1Н1ПВП 273 8. Выберите пункт главного меню Return в окне Settings. 9. Щелкните по графической кнопке Drawing. 10. Рисуйте эластичный контур мышью. Объект должен рисоваться с учетом свойств, заданных на вкладках диалогового окна Settings. 11. Выберите пункт главного меню Return в окне Drawing. 12. Выберите пункт меню Quit. 13. Окно закроется, так как приложение завершило работу. Пользовательский интерфейс для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект UserlnterfaceProgramPPC.vcw в папке UserlnterfaceProg- ramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу UserlnterfaceProgram. 12. Коснитесь стилосом графической кнопки Settings. Вы окажетесь в диало- говом окне Settings и увидите набор вкладок, причем открыта будет вкладка Lines. 13. Задайте какие-нибудь параметры на вкладке Lines. 14. Зайдите на другие вкладки и с помощью находящихся на них элементов управления задайте значения различных параметров рисования. При ка- сании стилосом заголовка вкладки программа должны скрыть текущую страницу и показать следующую. 15. Выберите пункт главного меню Return в окне Settings. 16. Коснитесь графической кнопки Drawing. 17. Рисуйте эластичный контур. Объект должен рисоваться с учетом свойств, заданных на вкладках диалогового окна Settings. 18. Выберите пункт главного меню Return в окне Drawing. 19. Выберите пункт меню Quit. 20. Окно закроется, так как приложение завершило работу.
Глава 10. Сохранение параметров в приложениях У каждой программы есть набор параметров. Когда пользователь входит в про- грамму, она обычно восстанавливает те значения параметров, которые были зада- ны во время последнего сеанса работы. В этой главе мы разработаем средства для сохранения параметров между запусками программы. Хотя эта задача важна сама по себе, попутно мы затронем и некоторые другие вопросы проектирования. Для сохранения параметров будет написана повторно используемая библиотека. С минимальными изменениями она позволит быстро встраивать аппарат сохранения параметров в любое приложение. Для этого мы выделим в библиотеке отдельные уровни. По мере прохождения через разные уровни программы данные будут преобразовываться в платформенно-независи- мую форму, а затем сохраняться в формате конкретного хранилища. В примерах продемонстрирована настройка нижнего уровня на три разных вида хранилищ: текстовые файлы, реестр и база данных для Pocket PC. Приводят- ся рекомендации по выбору наиболее подходящего для конкретной задачи спосо- ба хранения. В этой главе представлены три разные программы, но интерфейс у них общий. Он показан на рис. 10.1. Интерфейс состоит из единственного диалогового окна. Программа ведет ре- гистр, в котором хранится одно число с плавающей точкой. В каждой записи реГИ- ImHIe Manaqet Ptoqtam Qu» — INI-файл (текстовый)! Delete DBate I Реестр База данных для Pocket PC Рис. 10.1. Пользовательский интерфейс программы для сохранения параметров
Применение идеи многоуровневого дизайна НИИ 275 стра есть также имя владельца. Кроме того, база данных имеет номер версии, ко- торый устанавливается на этапе ее инициализации. В пользовательском интерфейсе можно выделить две области. Сверху нахо- дится область данных, в которой отображается текущее состояние параметров. В нижней части расположены кнопки, позволяющие выполнять операции над ба- зой данных. Область данных представлена в виде таблицы с двумя колонками. В левой колонке мы видим имена параметров, а в правой - поля, в которые можно ввести значения соответствующих параметров. Так, на рис. 10.1 значение параметра Owner равно Kreil. При первом запуске программы все поля ввода пусты. Для выполнения опера- ций над базой данных предназначены кнопки в нижней части окна. Типичная по- следовательность действий пользователя такова. 1. Нажать кнопку Open DBase для установления соединения с базой данных. 2. Нажать кнопку Read Record для считывания текущих значений параметров. 3. С помощью клавиатуры и стилоса изменить значения полей Owner и Register. 4. Нажать кнопку Write Record для записи новых значений параметров. 5. Нажать кнопку Close DBase для разрыва соединения с базой данных. 6. Нажать кнопку Open DBase для установления соединения с базой данных. 7. Нажать кнопку Read Record для считывания текущих значений параметров. Выполнив все эти действия, можно будет убедиться, что механизм сохране- ния и восстановления параметров работает правильно. Параметры можно сохранять в различных хранилищах. В этой главе мы рас- смотрим три версии программы, предназначенные для трех наиболее распростра- ненных хранилищ, перечисленных на рис. 10.1. В INI-файле данные хранятся в текстовом виде. В системном реестре данные хранятся в двоичном виде, про- смотреть их можно с помощью редактора реестра. И наконец, в Pocket PC встрое- на база данных для хранения индексированных записей в двоичном формате. О том, какой вид хранилища выбирать в конкретных условиях, рассказано в раз- деле «Выбор формата хранения». Применение идеи многоуровневого дизайна к решению задачи о хранении параметров Для обеспечения независимости от хранилища необходимо выделить в про- грамме отдельные уровни. На уровне приложения база данных параметров пред- ставляется индексированной таблицей. Каждая запись таблицы содержит имя и значение определенного типа. После того как база создана, программа может со- хранять и извлекать значения параметров, обращаясь к зависящим от типа функ- циям. Каждой функции передается индекс записи, а также имя и значение пара- метра, хранящегося в этой записи. Для примеров, рассматриваемых в этой главе, имеются следующие параметры:
276 Illi Сохра нение параметров в приложениях Индекс Имя Тип данных 1 DBVersion Double 2 Owner String 3 Register Double Первый параметр DBVersion позволяет менеджеру базы данных заменить уста- ревшую базу данных новой версией. Предполагается, что во всех базах данных, управляемых разработанной в данной главе библиотекой, в первой позиции нахо- дится номер версии. Остальные параметры зависят от конкретного приложения. На рис. 10.2 представлено разбиение библиотеки на уровни. Мы видим, как одна из видимых приложению функций реализуется на разных уровнях. На каж- дом уровне выполняется некоторая операция, необходимая для преобразования данных из формы, не зависящей от устройства, в форму, согласующуюся с требо- ваниями конкретного хранилища. Как видно из рис. 10.2, приложение взаимодействует напрямую с компонен- том ParameterDBMgr. В данном случае оно запрашивает значение с плавающей точкой, обращаясь к функции GetDoubleValue. Ей передается индекс записи, со- держащей нужный параметр, а в ответ функция возвращает имя и значение пара- метра, хранящегося в этой записи. Рис. 70.2. Уровни доступа к хранилищу ПРИМЕЧАНИЕ Поскольку в базе данных могут храниться также целочисленные и строковые параметры, компонент ParameterDBMgr предоставляет еще две функции: GetlntegerValue и GetStringValue. Ответственность за вызов подходящего метода доступа возлагается на программиста.
Применение идеи многоуровневого дизайна НИМ 277 Получив запрос на выборку значения с плавающей точкой, компонент Рага- meterDBMgr передает индекс позиции функции ReadRecordFromDBase, входящей в состав компонента DBRecordMgr. Основное назначение этого компонента - пре- образовывать данные из формата хранения в формат, ожидаемый приложением. Далее компонент DBRecordMgr передает запрос компоненту DBaseMgr, а тот уже напрямую реализует механизм доступа к конкретному хранилищу. Его цель - инкапсулировать все детали работы с хранилищем. ПРИМЕЧАНИЕ Для INI-файлов, реестра и встроенной в Pocket PC базы данных существуют раз- ные версии компонента DBaseMgr. Но смена формата хранения никак не отража- ется на верхних уровнях. Для извлечения данных из конкретного хранилища служит функция GetRecordFromDBase, предоставляемая компонентом DBaseMgr. Ради упроще- ния реализации DBaseMgr применяет стандартный формат хранения, для про- граммирования которого не нужно прилагать больших усилий. К тому же стан- дартный формат легко адаптируется к любому другому формату. Как показано на рис. 10.2, стандартный формат - это двоичный объект (blob) в сочетании с числом байтов в этом объекте. Итак, для считывания данных из хранилища уровни взаимодействуют сле- дующим образом: □ функция GetRecordFromDBase, принадлежащая компоненту DBaseMgr, извлекает двоичный объект; □ функция ReadRecordFromDBase, принадлежащая компоненту DBRecord- Mgr, преобразует двоичный объект в запись; □ функция GetDoubleValue, принадлежащая компоненту ParameterDBMgr, извлекает из записи значение с плавающей точкой. Пользуясь этими операциями, программист может без труда осуществлять инициализацию, чтение и запись в базу данных параметров, почти не заботясь о внутреннем формате хранения. На рис. 10.3 показано, в каком формате данные представлены на каждом уров- не. Справа от названия уровня приведена соответствующая ему структура данных. Приложение взаимодействует с базой данных параметров посредством мето- дов доступа, предоставляемых компонентом ParameterDBMgr. Как уже было ска- зано, для каждого типа данных имеются свои методы доступа. Методу доступа передаются номер позиции (начиная с 0), имя и значение параметра. Методы, поддерживаемые компонентом DBRecordMgr, носят более общий характер. Каждый из них оперирует записью, в которой хранятся имя параметра, индикатор типа и значение. Записи на этом уровне самодостаточны. Это набор числовых полей, полностью характеризующих параметр программы. На уровне DBaseMgr набор байтов представлен в виде двоичного объекта (то есть не имеет н-" ч внутренней структуры). Такое представление дает не-
Рис. 10.3. Преобразование данных на разных уровнях сколько преимуществ. Любое хранилище, доступное на платформе Pocket PC, может работать с двоичными объектами. Реализация почти не требует усилий, так как запрограммировать чтение и запись двоичных объектов очень просто. В этой главе рассматриваются три программы-примера. Отличаются они только реализацией уровня DBaseMgr. В одной данные хранятся в INI-файлах, в другой - в реестре, а в третьей - во встроенной базе данных Pocket PC. Выбор формата хранения Приложению для Pocket PC доступно три основных хранилища параметров: текстовые INI-файлы, реестр и встроенная база данных Pocket PC. У каждого формата есть сильные и слабые стороны. INI-файлы обладают одним существенным достоинством: их можно просмат- ривать и изменять в любом текстовом редакторе, как на настольном ПК, так и на Pocket PC. Это позволяет разработчику быстро создавать тестовые данные и сра- зу видеть, что программа выводит. Тем самым сокращается время отладки. Пользователю такая возможность сулит дополнительную гибкость, если он поче- му-либо захочет обойти графический интерфейс для задания параметров. Однако размещение параметров в одном или нескольких INI-файлах создает проблемы в плане управления конфигурацией и установки программы. В комп- лекте поставки должны присутствовать все INI-файлы, иначе установка может завершиться неудачно. Есть еще и проблема быстродействия. Текстовые файлы по своей природе линейны. Чтобы добраться до последнего параметра, программа должна прочитать все предшествующие. Если файл параметров велик, то такой последовательный доступ может занимать много времени, и производительность программы снизится. У реестра есть целый ряд достоинств. Это индексированное хранилище, по- этому доступ к конкретному параметру производится горадо быстрее, чем в слу- чае текстовых файлов. Для прямого доступа к данным в обход программы нужен специальный редактор реестра. Но с хранением данных в реестре связаны другие проблемы, и в первую оче- редь надежность. Реестр - это база данных о системе Windows г Если програм-
Настройка менеджера базы 18НПМ1 279 ма некорректно реализует операции с реестром, то вся система может перестать работать. И последний вариант - встроенная база данных Pocket PC. Она тоже индек- сирована, так что доступ к параметру занимает минимум времени. По этой причи- не встроенная база данных идеальна для многих приложений. Но это двоичная база, существующая только на Pocket PC. Прямой доступ к хранящимся в ней па- раметрам невозможен без написания специальных программ. Хуже того, если программа запишет в базу некорректные данные, то исправить их невозможно без полной перезагрузки, в результате которой будут стерты все установленные про- граммы и данные. И если вы решите использовать этот формат, то тестировать программу на настольном ПК будет затруднительно. ПРИМЕЧАНИЕ Программам, предназначенным для Pocket PC, лучше всего подходят текстовые INI-файлы. Это наиболее переносимое и удобное для отладки решение, так как текстовые файлы легко редактировать. Обычно доступ к базе данных производится лишь дважды на протяжении вза- имодействия пользователя с одним окном интерфейса. При входе в окно пара- метры считываются из базы в глобальную область, управляемую компонентом DataMgr, а при выходе модифицированные значения записываются обратно в базу. В результате манипуляций с элементами управления новые значения пара- метров заменяют старые, находящиеся под управлением DataMgr. Если бы мы вместо этого сразу записывали их в хранилище, то время реакции оказалось бы недопустимо большим. Поскольку из соображений производительности доступ к хранилищу должен производиться только при входе и выходе из окна, то он по необходимости оказывается последовательным. Средства индексирования, пред- лагаемые реестром и встроенной базой данных, при таком подходе излишни. А раз индексированный доступ не нужен, то для хранения параметров лучше прибег- нуть к INI-файлам, хотя бы в силу их удобства для отладки. Настройка менеджера базы данных параметров Перед тем как приступать к настройке менеджера базы данных параметров под конкретное приложение, разработчик должен выполнить ряд шагов. 1. Выбрать конкретный формат хранения. 2. Сконфигурировать файл DBaseMgr.c для выбранного формата. 3. Добавить в проект файлы DBaseMgr.h, DBaseMgr.c, DBFieldMgr.h, DBFieldMgr.c, DBRecordMgr.h, DBRecordMgr.c, ParameterDBMgr.h, ParameterDBMgr.c. 4. Добавить файлы, необходимые для настройки DBaseMgr.c на конкрет- ный формат хранения, например: StrMgr.h, StrMgr.c, PortabilityUtils.h и PortabilityUtils.c.
280 1111 Сохранение параметров в приложениях После конфигурирования проекта и описанных ниже шагов настройки прило- жение будет взаимодействовать с конкретным хранилищем. Для настройки менеджера базы данных на конкретное приложение выполни- те следующие действия. 1. В файле ParameterDBMgr.h определите структуру записи в базе данных параметров. 2. В файле ParameterDBMgr.c определите записи по умолчанию для каждого параметра. 3. В разные части программы добавьте обращения к функциям компонента ParameterDBMgr для взаимодействия с базой данных параметров. Если эти требования показались вам до смешного простыми, то это заслуга описанного в предыдущем разделе разбиения на уровни. Коль скоро компонент DBaseMgr настроен на конкретное хранилище, то вся сложная работа позади. Верхние уровни будут прекрасно работать вне зависимости от того, какие преоб- разования выполняются на нижнем. Пример настройки менеджера базы данных параметров В этом разделе мы продемонстрируем шаги процедуры настройки. Эти шаги одинаковы для всех трех рассматриваемых в данной главе программ. Определение структуры записи в базе данных параметров Для определения структуры записи нужно объявить символические констан- ты, которые будут использоваться в разных частях программы. Следующие кон- станты объявлены в файле ParamenterDBMgr.h и годятся для всех трех программ. ♦define CurrentDBVersion 1.0 ♦define Numberparameters 3 ♦define DBVersionRecordNumber 0 ♦define OwnerNameRecordNumber 1 ♦define RegisterValueRecordNumber 2 Константа CurrentDBVersion должна быть объявлена в любом приложении. Она позволяет одному из нижних уровней выяснить, изменилась ли организация базы данных. Если текущий номер версии отличается от значения этой констан- ты, то программа удаляет старую базу и создает вместо нее новую с той структу- рой записи и значениями по умолчанию, которые будут описаны в следующем разделе. Для обхода записей базы данных нижние уровни должны знать, сколько пара- метров в ней хранится. Эта информация передается с помощью константы NumberParameters. Далее нужно определить символы, соответствующие индексам записей о па- раметрах в базе данных. По соглашению первая запись имеет индекс 0 и ей соот-
281 Пример настройки менеджера базы ветствует константа DBVersionRecordNumber. Нижние уровни хранят в этой за- писи номер версии базы данных. Остальные записи содержат собственно параметры приложения. Разработчик может выбирать для описывающих их констант любые имена, лишь бы они отра- жали назначение параметра. Обычно имена всех констант заканчиваются строкой RecordNumber. Позже программа будет передавать эти константы различным ме- тодам доступа из компонента ParamenterDBMgr для чтения и записи параметров. Определение записей по умолчанию для каждого параметра Выше уже было сказано, что нижние уровни автоматически выполняют созда- ние и конфигурирование базы данных параметров. Если база еще не существует, она будет создана. При изменении версии старая база удаляется и создается но- вая. Чтобы эти операции выполнялись корректно, необходимо предоставить определения записей, содержащие значения параметров по умолчанию. Ниже приведен пример таких записей: void SetDefaultValues(void) { SetDoubleRecordintoParameterDBase(DBVersionRecordNumber, _____TEXT("Version"), DoubleValue, CurrentDBVersion ) ; SetstringRecordlntoParameterDBase(OwnerNameRecordNumber, _____TEXT("Owner") , Stringvalue, _TEXT("Kreil") ) ; SetDoubleRecordintоParameterDBase(RegisterValueRecordNumber, _____TEXT("Register") , DoubleValue, 1.0 ) ; } Здесь для каждого параметра определена запись, содержащая его значение по умолчанию. Методы доступа предоставляет компонент ParameterDBMgr. Для каждого типа данных имеется пара методов Get и Set. Очевидно, что в данном случае нам нужен лишь метод Set. У каждого метода доступа есть четыре аргумента. Первым аргументом вызыва- ющая программа передает номер записи. В примере выше номерами служат опреде- ленные ранее символические константы. Остальные аргументы - имя параметра, числовой код типа данных и значение. Тип значения должен соответствовать имени метода. Так, если вызывается метод SetDoubleRecordlntoParameterDBase, то после- дний аргумент должен иметь тип double. Вначале всегда должен записываться номер версии в запись с индексом DBVersionRecordNumber. В качестве значения этого параметра следует лепетать константу CurrentDBVersion. Использование функций для взаимодействия с базой данных параметров Необходимость в доступе к базе возникает в результате различных действии пользователя Когда программа открывает окно, в котором показываются зна" ’
282 ПИ Сохранение параметров в приложениях ния параметров, их нужно сначала прочитать из базы. А после модификации но- вые значения следует записать обратно в базу. Следующий фрагмент диалоговой процедуры встречается во всех трех программах из этой главы: void DlgOnCommand ( HWND hDlg, int ilD, HWND hDlgCtl, UINT uCodeNotify ) { switch( ilD ) { case IDCJBUTTON1: GetValuesFromParameterDBase(hDlg) ; break ; case IDC_BUTTON3: OpenParameterDBase() ; break ; case IDC_BUTTON4: CloseParameterDBase () ; break ; } } В общем случае приложение выполняет четыре основные операции с базой данных параметров: открытие, считывание данных о параметрах, запись данных и закрытие. Соответственно в предложении switch должно быть четыре ветви. Когда пользователь нажимает ту или Иную кнопку, обработчик сообщения WM_COMMAND выполняет затребованную операцию. Этот код не зависит от формата хранения. Методы OpenParameterDBase и CloseParameterDBase принадлежат уровню ParameterDBMgr. Когда пользователь запрашивает операцию чтения, обработчик кнопки IDC_BUTTON1 вызывает локальную вспомогательную функцию. Вот ее код: void GetValuesFromParameterDBase(HWND hDlg) { TCHAR Owner[256] ; double Register ; GetStringValueFromParameterDBase(OwnerNameRecordNumber, Owner) ; GetDoubleValueFromParameterDBase(RegisterValueRecordNumber, ^Register) ; SetStringlntoTextWindow(hDlg,IDC_EDIT2,Owner) ; SetDoubleIntoTextWindow(hDlg,IDC_EDIT3,Register,2) ; } ’ Здесь вызываются методы компонента ParameterDBMgr для считывания дан- ных из базы. Параметр Owner строковый, поэтому для извлечения номера вла- дельца применяется метод GetStringValueFromParameterDBase. А для извлече- ния значения параметра Register с плавающей точкой нужно обратиться к методу GetDoubleValueFromParameterDBase. Этот код абсолютно не зависит от формата хранения. Считанные значения параметров функция заносит в поля ввода, чтобы пользователь мог ими манипулировать. Для решения этой несложной задачи она вызывает функции, принадлежащие компоненту GUIUtils. Например, функция
283 Обзор реализации уровней SetDoublelntoTextWindow копирует значение параметра Register из локальной переменной в поле ввода. Она принимает описатель родительского окна (hDlg), идентификатор элемента управления (IDC_EDIT3), значение параметра и число десятичных цифр после запятой (2). Обзор реализации уровней В этом разделе мы кратко рассмотрим уровни программного обеспечения, от- ветственные за работу с базой данных параметров, в том порядке, в каком они изображены на рис. 10.2. Работа с базой данных начинается в обработчике какого-то сообщения, напри- мер WM_COMMAND, с обращения к функции из компонента ParameterDBMgr: GetDoubleValueFromParameterDBase( RegisterValueRecordNumber, ^Register ) ; Внутри ParameterDBMgr эта функция реализована следующим образом: static DBRecordType Record ; void GetDoubleValueFromParameterDBase( int Position, double * Value ) { if (DBaselsOpen) { Record = CreateRecord() ; ReadRecordFromDBase(Record,Position) ; GetDoubleValueFromRecord(Record,Value) ; DestroyRecord(Record) ; ) } Сначала проверяется, открыта ли база данных. Флаг DBaselsOpen поднима- ется, когда клиентская программа вызывает функцию открытия базы данных. Основная структура данных на следующем уровне - запись. Установив со- единение с базой данных, функция создает экземпляр абстрактного типа данных DBRecordType. Значение этого типа хранится в статической переменной Record. Вызов функции ReadRecordFromDBase заносит в нее значения, хранящиеся в за- писи с указанным номером. Затем для извлечения значения параметра из запол- ненной записи вызывается функция GetDoubleValueFromRecord. И в конце рабо- чая запись уничтожается путем обращения к функции DestroyRecord. Обратите внимание на простоту и понятность этой функции. Ее код читается как обычный текст на английском языке. Даже человек, который видит эту функ- цию впервые, легко разберется в потоке выполнения. Все это благодаря удачному разбиению программы на уровни. Наша основная цель - опустить детали реали- зации как можно ниже. Если этой цели удается достичь, то программа становится простой для понимания. А в долгосрочной перспективе это сокращает число оши- бок и время отладки. void ReadRecordFromDBase( DBRecordType Record, int Position ) ( BYTE * Buffer ; int Recordsize ; RecprdSize = GetDBRecordSize() ;
£££|^|ИНН||| Сохранение параметров в приложениях Buffer = CreateEmptyRecordBuffer() ; (jetRec°rdFr°mDBase (Position,Buffer,RecordSize) ; QOpyRecordBufferlntoRecord(Record,Buffer) ; DestroyRecordBuffer(Buffer) ; } Эта функция, расположенная на уровне DBRecordMgr, использует для временного хранения в буфере. В данном случае запись в буфере - просто неструктурированный массив байтов. Массив считывается в буфер функцией GetRecordFromDBase. Затем функция CopyRecordBufferlntoRecord копирует байты из буфера в запись, предоставленную вызывающей программой. Функция GetRecordFromDBase взаимодействует с физическим хранилищем и зависит от формата. Ее текст приведен ниже: void GetRecordFromDBase( int Index, BYTE * Data , int Count ) { TCHAR IndexName[256] ; TCHAR CurrentName[256] ; int BlobSize ; BYTE BlobData[MAX_BLOB_BYTES] ; int Counter ; Counter - 0 ; while (Counter <= (Index — 1)) { ConvertlntToString(Counter,CurrentName) ; RemoveCharactersFromFront(CurrentName,1) ; ReadlntPropertyFromFile(CurrentDBase,CurrentName,SBlobSize) ; ReadBlobFromFile(CurrentDBase,BlobData,BlobSize) ; Counter = Counter + 1 ; } ConvertlntToString(Index,IndexName) ; RemoveCharactersFromFront(IndexName,1) ; ReadlntPropertyFromFile(CurrentDBase,IndexName,SBlobSize) ; ReadBlobFromFile(CurrentDBase,BlobData,BlobSize) ; if (Count < BlobSize ) memcpy(Data,BlobData,Count) ; else memcpy(Data,BlobData, BlobSize) ; SetFilePointer(CurrentDBase,0,0,FILE_BEGIN) ; } Этот код предназначен для считывания данных из текстового INI-файла. Функция читает все записи подряд, пока не дойдет до записи с указанным но- мером. На каждой итерации цикла запись считывается, хранящийся в ней индекс преобразуется в целое число, и проверяется, прочитано ли уже нужное число предшествующих записей. После того как запись обнаружена, функция извлекает из нее байты данных и помещает их в буфер, предоставленный вызывающей про- граммой.
285 Конфигурирование нижнего уровня Конфигурирование нижнего уровня для конкретного хранилища В последней функции из предыдущего раздела было показано, как компонент DBaseMgr адаптируется к линейной структуре INI-файла. Для сравнения приве- дем реализацию той же функции GetRecordFromDBase для доступа к хранилищу другого вида, а именно к реестру Windows СЕ: void GetRecordFromDBase( int Index, BYTE * Data , int Count ) { TCHAR IndexName[256] ; DWORD Type; DWORD BlobCount ; BYTE BlobData[MAX_BLOB_BYTES] ; ConvertlntToString(Index,IndexName) ; RegQueryValueEx) CurrentDBase, IndexName, NULL, SType, BlobData, SBlobCount); if (Count < (int)BlobCount ) memcpy(Data,BlobData,Count) ; else memcpy(Data,BlobData, BlobCount) ; } Здесь всего три простых шага. Сначала номер нужной записи Index преобра- зуется из числа в строку IndexName. Далее вызывается функция Win32 API RegQueryValueEx, которой в качестве ключа передается строка IndexName. Эта функция возвращает двоичный объект BlobData, хранящийся в реестре. И на по- следнем шаге этот объект копируется в буфер Data, предоставленный вызываю- щей программой. Сравните эту реализацию с предыдущей. Она существенно короче. Кроме того, данные, хранящиеся в реестре, автоматически индексируются для быстрого доступа. DBaseMgr пользуется этим, используя в качестве ключа переданный но- мер записи, а не какое-то произвольно выбранное значение. Резюме Цель настоящей главы - продемонстрировать, как можно выделить в про- грамме уровни для эффективной инкапсуляции доступа к конкретному хранили- щу параметров. Вот что следует запомнить: □ для хранения параметров у программы на платформе Pocket PC есть не- сколько возможностей; □ у каждого формата хранения есть сильные и слабые стороны; □ разбиение на уровни позволяет легко адаптировать программу к конкрет- ному формату хранения; □ на каждом уровне данные преобразуются в форму, обеспечивающую неза- висимость от особенностей хранилища;
286 Illi Сохранение параметров в приложениях □ на самом нижнем уровне данные рассматриваются как неструктурирован- ный массив байтов; □ представление данных в виде двоичного объекта на нижнем уровне упро- щает реализацию. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка Хранение в INI-файле для настольного ПК Хранение в INI-файле для Pocket PC Хранение в реестре для настольного ПК Хранение в реестре для Pocket PC Хранение во встроенной базе данных Pocket PC для настольного ПК Хранение во встроенной базе данных Pocket PC для Pocket PC IniFileManagerProgram IniFileManagerProgramPPC RegistryManagerProgram RegistryManagerProgramPPC Отсутствует DBaseManagerProgramPPC Инструкции по сборке и запуску Хранение в INI-файле для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект IniFileManagerProgram.dsw в папке IniFileManagerProgram. 3. Соберите программу. 4. Запустите программу. 5. Нажмите кнопку Open DBase для открытия базы данных. При первом за- пуске программа создаст файл параметров со значениями по умолчанию. Файл будет называться ParameterDB. 6. Нажмите кнопку Read Record для считывания записей из базы данных. Если это первый запуск, то значения будут такими, как на рис. 10.1. 7. Измените значения в полях Owner и Register. 8. Нажмите кнопку Write Record для записи новых данных в базу. 9. Нажмите кнопку Close DBase для закрытия базы данных. 10. Выберите пункт меню Quit. 11. Окно закроется, так как приложение завершило работу. 12. Запустите программу снова. 13. Нажмите кнопку Open DBase для открытия базы данных. 14. Нажмите кнопку Read Record для считывания записей из базы данных. Должны появиться те значения, которые вы записали в базу во время пре- дыдущего сеанса. 15. Нажмите кнопку Close DBase для закрытия базы данных. 16. Выберите пункт меню Quit.
Примеры программ в Web Н111ПН1 287 17. Окно закроется, так как приложение завершило работу. Хранение в INI-файле для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект IniFileManagerProgramPPC.vcw в папке IniFileManager- ProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу IniFileManagerProgram. 12. Коснитесь стилосом кнопки Open DBase для открытия базы данных. При первом запуске программа создаст файл параметров со значениями по умолчанию. Файл будет называться ParameterDB. 13. Коснитесь стилосом кнопки Read Record для считывания записей из базы данных. Если это первый запуск, то значения будут такими, как на рис. 10.1. 14. Измените значения в полях Owner и Register. 15. Коснитесь стилосом кнопки Write Record для записи новых данных в базу. 16. Коснитесь стилосом кнопки Close DBase для закрытия базы данных. 17. Выберите пункт меню Quit. 18. Окно закроется, так как приложение завершило работу. 19. Запустите программу снова. 20. Коснитесь стилосом кнопки Open DBase для открытия базы данных. 21. Коснитесь стилосом кнопки Read Record для считывания записей из базы данных. Должны появиться те значения, которые вы записали в базу во время предыдущего сеанса. 22. Коснитесь стилосом кнопки Close DBase для закрытия базы данных. 23. Выберите пункт меню Quit. 24. Окно закроется, так как приложение завершило работу.
нам МНН I Глава 11. Многопоточные приложения и синхронизация Обычно, говоря о многопоточной программе, подразумевают, что ее части испол- няются одновременно. Это утверждение ложно. Конечно, внешнему наблюдате- лю кажется, что различные участки программы работают параллельно. Но на са- мом деле все обстоит не совсем так. Одна из задач данной главы - объяснить, что же в действительности скрывается за многопоточным исполнением. Рассмотрим, по каким причинам прибегают к использованию потоков. Пользователь хотел бы распечатать файл. Если бы программа занималась только этим, пользователь был бы недоволен. Ведь тогда он не смог бы взаимодейство- вать с программой на протяжении всего времени печати. А распечатка достаточно большого файла может занять несколько часов. Представьте радость пользовате- ля от того, что все это время работа стоит! Но по зрелом размышлении проблема оказывается простой. Программа долж- на одновременно обслуживать два внешних интерфейса: пользователя и принтер. Если разработчик сумеет это организовать, то пользователю будет казаться, что две части программы работают параллельно. Необходимость в обслуживании сразу нескольких интерфейсов возникает по- стоянно. В качестве примера можно привести обслуживание нескольких сетевых портов, управление различными устройствами, скажем, лазерами или системами автоматизированного производства, загрузку данных с жесткого диска во встро- енные контроллеры и множество других задач. Иногда программа должна выпол- нять сложные вычисления, не прерывая обслуживание пользователей и одновре- менно управляя аппаратными интерфейсами. Разумное и неразумное применение потоков Поток - это независимая единица планирования. Центральный процессор по- току выделяет ядро Windows СЕ. За счет использования нескольких потоков про- грамма может одновременно обслуживать разные интерфейсы. Но это не означа- ет, что части программы действительно выполняются параллельно. ПРИМЕЧАНИЕ Ядро Windows СЕ - это сокращенная версия ядра, входящего в состав операци- онных систем Windows 2000 и Windows ХР. На рис. 11.1 показана временная диаграмма выполнения двух потоков в гипо- тетической программе.
Разумное и неразумное применение потоков UlMMIMil 289 Одно приложение Временная диаграмма выполнения Вытесняющий круговой планировщик в ядре Windows СЕ Рис. 11.1. Временная диаграмма выполнения потоков Предполагается, что представленная на этом рисунке программа исполняет два потока. Поток пользовательского интерфейса принимает команды от пользо- вателя, а поток диспетчера печати в то же время распечатывает большой файл. Справа приведена временная диаграмма выполнения программы. На диаграмме показаны периоды времени, в течение которых каждый поток владеет процессором. Мы видим, что, попользовавшись процессором некоторое время, один поток передает его другому. Акт передачи называется контекстным переключением. Именно это переключение и совместное пользование процессо- ром и создают эффект кажущейся параллельности. ПРИМЕЧАНИЕ Потоки не исполняются параллельно. Хотя со стороны представляется, что про- грамма одновременно поддерживает несколько интерфейсов, в любой момент времени процессор находится в распоряжении только одного потока. В конце концов, Pocket PC оборудована лишь одним процессором. Пока процессор находится в распоряжении некоторого потока, этот поток может решать свою задачу. Поток пользовательского интерфейса может принять и выполнить команду пользователя, а поток диспетчера печати - вывести оче- редную порцию файла. Ни один интерфейс не оказывается обделенным. Пользо- ватель не сетует на то, что программа игнорирует касания стилосом, поскольку контекстное переключение происходит достаточно часто и поток интерфейса по- лучает процессор
290 НИ Многопоточные приложения и синхронизация В системе Windows СЕ поток продолжает выполняться, пока не истечет вы- деленный ему квант времени или он не выдаст запрос на ввод / вывод данных. Затем происходит контекстное переключение. Такое принудительное переклю- чение называется вытесняющей многозадачностью. О.тбирая процессор у пото- ка, ядро гарантирует, что каждый поток (а следовательно, и управляемый им интерфейс или выполняемый алгоритм) получит справедливую долю процес- сорного времени. Состояния потока Каждый поток может находиться в одном из нескольких состояний. От состоя- ния потока зависят поведение и производительность приложения. На рис. 11.2 приведена диаграмма переходов состояний потока. Каждый прямоугольник представляет состояние. Стрелка обозначает пере- ход из одного состояния в другое. Текст рядом со стрелкой описывает условие, при котором происходит переход. Одно из самых интересных состояний - «Выполняется». В этом состоянии поток владеет процессором. Из него исходят три стрелки, следовательно, есть три причины для выхода из этого состояния. Если истекает выделенный потоку квант времени или поток вытесняется более приоритетным потоком, то он пере- ходит в состояние «Готов». Если же поток выполнил все, что от него требова- лось, то он переходит в состояние «Завершен». И наконец, когда поток должен ожидать синхронизации с другим потоком, он переходит в состояние «Ожида- ние». Самыми важными на рис. 11.2 являются переходы между состояниями «Го- тов», «Следующий» и «Выполняется». Согласно диаграмме, перед тем как гото- вый поток получит процессор, должно произойти два события. Во-первых, этот поток должен быть выбран для выполнения из множества всех готовых потоков. А затем поток, владеющий процессором, должен еще выйти из состояния «Выпол- няется». Поскольку то и другое происходит не мгновенно, выполнение потока может быть задержано на относительно длительное время. Но разработчик может сократить задержку, если будет правильно работать с потоками. ПРИМЕЧАНИЕ ........................._____............. ..... Большей части задержек можно избежать, если свести к минимуму число потоков в программе. Опыт разработки многопоточных приложений показывает, что оп- тимально иметь от семи до десяти тщательно отобранных и спроектированных потоков. Чаще всего потоки применяются для мониторинга аппаратных интерфейсов, контроллеров, принимающих решения, реализации алгоритмов и диспетчеров пользовательских интерфейсов. Во всех этих случаях необходима какая-то форма обмена сообщениями или сигналами между потоками.
Разумное и неразумное применение потоков ЩЦНННН 291 CreateThread Выполнение завершено Завершен Процесс закончил работу Рис. 11.2. Диаграмма перехода состояний потока Планирование потоков Когда поток переходит в состояние «Готов», ядро помещает его в очередь го- товых потоков. Организация этой очереди изображена на рис. 11.3. Очередь состоит из нескольких кластеров. Каждый кластер соответствует од- ному приоритету. Потоки, находящиеся в кластере 0, имеют самый низкий при- оритет. Потоки с наивысшим приоритетом 31 оказываются в верхнем кластере. Процедура выбора ядром потока для выполнения состоит из двух шагов. Сна- чала ищется первый непустой кластер, начиная с номера 31. Поток, находящийся в начале очереди в этом кластере, и будет выполняться следующим. Это означает, что никакой поток не сможет начать выполнение, если в очереди имеются потоки с более высоким приоритетом. Напомним, что поток может вернуться в очередь готовых, если исчерпает свой квант времени или будет вытеснен более приоритетным потоком. При этом он ставится в конец очереди в том кластере, который соответствует его приоритету. СОВЕТ Этот алгоритм называется круговым (round-robin) планированием на основе при- оритетов согласно дисциплине «первым пришел - первым обслужен». Перемещая вытесненный поток из начала очереди в конец, ядро реализует круговую дисциплину обслуживания.
292 1111 Многопоточные приложения и синхронизация Приоритеты Очередь готовых потоков потоков Наивысший Наинизший Чтобы низкоприоритетный поток мог начать выполнение, все высокоприоритетные кластеры должны быть пусты Каждый кластер обслуживается по кругу согласно дисциплине «первым пришел — первым обслужен» Потоки реального времени освобождают процессор на время операций ввода/вывода Рис. 11.3. Выбор из очереди готовых потоков С точки зрения ядра, существует две основные категории приоритетов. При- оритеты с номерами от 16 до 31 относятся к категории реального времени, осталь- ные (от 0 до 15) - к категории переменных. Производительность существенно за- висит от того, в какую категорию попадает приоритет потока. Если это поток реального времени, то его приоритет остается неизменным на протяжении всего времени выполнения, если только сам поток добровольно не изменит его. Если же приоритет переменный, то ядро будет периодически корректировать его для по- вышения общего времени реакции системы. Управление приоритетами На рис. 11.4 показана процедура корректировки приоритетов. Как видите, приоритет потока может быть выше или ниже базового значения. Когда поток с переменным приоритетом начинает выполнение, ядро при- сваивает ему некий начальный приоритет, который определен по умолчанию в Windows СЕ. Первоначально приоритет потоков поднимается выше базового уровня, обычно на четыре пункта. Но по мере того как поток потребляет процес- сорное время, его приоритет опускается ниже базового. Как правило, нижний уро- вень приоритета на три пункта ниже базового. Начиная с этого момента, приори- тет потока снова начинает повышаться, и весь цикл повторяется заново.
Разумное и неразумное применение потоков ||||НМ| 293 Переменные приоритеты с - (6 Базовый приоритет процесса Наследуется от процесса 4 Динамический приоритет потока Во время взаимодействия с пользователем Со временем постепенно понижается ------------------------------------------ Базовый приоритет потока Во время интенсивных вычислений _______________________________________▼ Рис. 11.4. Переменные приоритеты потока Этот алгоритм понижения приоритета, по существу, штрафует потоки, по- требляющие много процессорного времени. Обоснование его очевидно. На ран- них этапах работы поток, скорее всего, занят получением входной информации от пользователя. Рост потребления ЦП, вероятно, означает, что взаимодействие с пользователем закончено, и поток приступил к сложным вычислениям. Из-за понижения приоритета вычисление займет больше времени, зато другие потоки, имеющие более высокий приоритет, смогут взаимодействовать с пользователем. ПРИМЕЧАНИЕ Многопоточное приложение лучше делать нейтральным по отношению к приорите- там. Иными словами, программа не должна изменять приоритет самостоятельно. Как бы разработчик ни пытался модифицировать приоритеты потоков, мало- вероятно, что это повысит производительность приложения, поскольку ядро все равно выровняет их. Конечно, программа может повысить свой приоритет до уровня реального времени. Но тогда могут быть заморожены все потоки в других программах, и рано или поздно кто-нибудь предъявит претензии. Выявив «пожирателя» време- ни, пользователи засыплют администратора жалобами. Демонстрация влияния приоритетов Ничто так не помогает разобраться в проблеме, как иллюстрация. На сайте книги (http://www.osborne.com) есть программа Bouncing Square, которая ви- зуально демонстрирует влияние приоритетов. Как и всегда, мы предлагаем ее вер- 'ии для настольного ПК и Pocket PC.
ПН Многопоточные приложения и синхронизация Идея программы очень проста. Пользователь создает в клиентской области окна несколько «прыгающих квадратиков». Каждый квадратик скачет вверх- вниз. При этом вертикальная координата уменьшается, так что в конце концов квадратик покидает окно. Вычислением вертикальной координаты и отображе- нием квадратика занимается отдельный поток. Каждому такому потоку пользова- тель может сам назначить приоритет. Меняя значения приоритетов, можно на- блюдать алгоритм управления приоритетами в действии. На рис. 11.5 показан интерфейс программы Bouncing Square. Главное меню состоит из двух пунктов. Справа находится пункт Priority Пе- ред запуском одного или нескольких потоков пользователь выбирает из этого меню какой-то приоритет. Приоритеты разбиты на две группы, между которыми стоит разделитель. Те, что выше разделителя, соответствуют числовым значениям, определенным в Win32 API. А под разделителем находится всего один пункт - Friendly, пред- ставляющий «вежливый» приоритет. Поток с таким приоритетом обязуется доб- ровольно освободить процессор, один раз обновив положение квадратика. Реали- зуется это путем вызова функции Sleep с параметром 0 миллисекунд. При этом поток помещается в очередь готовых, а процессор передается другому потоку на усмотрение ядра. Каждому новому потоку будет назначаться последний выбран- ный из меню приоритет. Для добавления нового квадратика используется стилос. На рис. 11.6 показа- но, как выглядит окно во время выполнения программы. Bouncing Square Program Quit ["Priority Highest Above Nomal Nonna! Below Llama! Lowest Эти приоритеты контролирует Windows СЕ Добровольно освобождает процессор Рис. 11.5. Задание приоритетов управляющих потоков в программе Bouncing Square На этом рисунке показаны два квадратика. Пользователь может создавать квадратики с разными приоритетами, выбрав предварительно нужный приоритет из меню. При этом можно наблюдать, как поведение квадратика зависит от при-
Введение в проблему синхронизации IIIIHHHHHI 295 Bouncing Square Program Quit Priority '-Л-.; I г — 1. Запустить поток, коснувшись стилосом любой точки — 2. Положение обновляется отдельными потоками Каждому управляющему потоку назначается приоритет, выбранный перед его запуском Рис. 11.6. Влияние приоритетов потоков на производительность оритета. Чем ниже приоритет, тем медленнее движется квадратик. Потоки, имею- щие приоритет Highest (Наивысший), лишают остальных доступа к процессору, так что управляемые ими квадратики практически стоят на месте. ПРИМЕЧАНИЕ Лучший способ поглядеть, как приоритеты влияют на выполнение, - назначить одному потоку приоритет Normal, а нескольким остальным - Below Normal. Введение в проблему синхронизации Использование потоков позволяет программе обслуживать несколько хозяев (интерфейсов). Однако ничто не дается даром. Некорректный доступ к разделяе- мым данным со стороны потоков может стать причиной ошибок. У этой проблемы есть несколько названий. Обычно программисты говорят о проблеме синхрониза- ции, или безопасности относительно потоков. Но суть от названия не зависит. Данные, к которым обращаются несколько потоков, могут быть испорчены. По- этому при проектировании таких программ нужно принимать специальные меры. Чтобы понять, как может возникнуть ошибка, рассмотрим простую програм- му, в которой будет всего два потока. Поток А записывает данные в некоторую разделяемую структуру RawData, а поток В считывает из нее данные и увеличива- ет значение. Поток А порождает входные значения в цикле и помещает в RawData счетчик цикла, умноженный на 10. Поток В умножает находящееся в RawData значение
296 Illi Многопоточные приложения и синхронизация на 20 и оставляет его там же. Если бы все работало нормально, то главный поток программы должен был бы читать из RawData значения счетчика цикла, умно- женные на 200. Пример, конечно, искусственный, но он моделирует проблемы, возникающие в реальной многопоточной программе. Например, некоторый поток может в цик- ле получать данные от устройства и помещать их в буфер. Другой поток читает данные из буфера и обрабатывает их. Это упрощенное описание реальной про- граммы мониторинга оборудования очень похоже на то, что происходит в приве- денном выше примере. На первый взгляд, потоки можно запрограммировать следующим образом: // Поток А for (Index = 0; Index < 20; Index = Index + 1) RawData[Index] = 10 * Index ; // Поток В for (Index = 0; Index < 20; Index = Index + 1) RawData[Index] = 20 * RawData[Index] ; Но представим себе, какой код мог бы быть сгенерирован для некоей вирту- альной машины, показанной на рис. 11.7. На рисунке представлена структура RawData. Слева находится поток Д кото- рый записывает в нее данные, а справа - поток В, который читает данные, прежде чем поток А обновит их. Интерес представляет показанный под каждым потоком код, который мог бы быть сгенерирован для типичного компьютера. Сначала рассмотрим поток А Его исполнение начинается с умножения индекса j на 10 и сохранения результата в регистре (reg). В следующей строке определяется адрес, по которому надо запи- сать данные (trgt). Это базовый адрес RawData плюс смещение, вычисляемое по индексу). Затем в ячейку памяти по целевому адресу копируется содержимое регист- ра (*reg). Аналогичный код генерируется для потока В, считывающего данные. RawData Потенциальные точки вытеснения Рис. 11.7. Пример проблемы синхронизации
Введение в проблему синхронизации IIIIHH 297 Приглядитесь к этому коду внимательнее. Одна строка, в которой вычисляет- ся и записывается новое содержимое RawData, транслируется в три машинные команды. После выполнения любой из них может произойти контекстное пере- ключение, заставляющее поток А отдать процессор. Эти три потенциальные точ- ки вытеснения обозначены Д В и С. Если программа создаст два потока и выполнит этот код примерно 20 раз, можно будет наблюдать странные вещи. Иногда действительно появляются пра- вильные значения, кратные 200. А иногда только кратные 10, как если бы поток А записал в RawData начальные значения уже после того, как поток В переписал их. Это и есть эффект несинхронизированного вытеснения. Чтобы лучше понять, как такое возможно, взгляните на табл. 11.1. Таблица 11.1. Анализ точек вытеснения 10 * j еще не записано потоком А Поток В читает мусор и умножает его на 20 Поток В записывает мусор, умноженный на 20 Поток А записывает 10 * j 10 * j еще не записано потоком А Поток В читает мусор и умножает его на 20 Поток В записывает мусор, умноженный на 20 Поток А записывает 10 * j © 10 * j записано потоком А Поток В читает значение 10 * j и умножает его на 20 Поток В записывает значение 200 * j В правой колонке показано, что произойдет, если поток А будет вытеснен в соответствующей точке. Предполагается, что после вытеснения поток В успеет выполнить все команды, показанные на рис. 11.7. Допустим, что поток А был вытеснен в точке А К этому моменту он уже вы- числил значение 10 * j, но оно еще находится в регистре. При контекстном пере- ключении содержимое регистра запоминается где-то в недрах ядра. Теперь исполняется поток В и по предположению успевает выполнить все ко- манды, показанные на рис. 11.7. В результате по целевому адресу в RawData будет записан мусор, ведь у потока А еще не было возможности записать туда то, что он вычислил. А как известно всякому программисту, мусор на входе дает мусор на выходе. В этот момент ядро прерывает поток В, и в результате контекстного пере- ключения в регистр записывается ранее сохраненное значение. Вернувшись к жизни, поток А возобновляет выполнение со следующей ко- манды, каковой является точка В. Если повезет, поток А закончит шаги В и С. Если теперь программа распечатает содержимое ячейки массива RawData, то мы увидим, что оно равно значению индекса, умноженному только на 10. По- скольку поток В был вытеснен в середине последовательности вычисления и за- писи данных, то операция не завершилась, как ожидалось. На этом примере мы наблюдаем возможную порчу данных из-за проблемы вытеснения или небезопа<'1' ню относительно потоков поведения. Чтобы ее разре-
298 1111 Многопоточные приложения и синхронизация шить, нужно найти какой-то способ свести последовательность порождения и за- писи данных к одной атомарной операции. Атомарным называется набор дей- ствий, которые должны быть выполнены как единое целое без разбиения на более мелкие операции. Это предотвратило бы доступ к RawData со стороны потока В до тех пор, пока поток А не заполнит весь массив. Подумайте, какие ужасные вещи могут произойти из-за вытеснения в реальном приложении. Вспомните пример потока, который следит за некоторым устрой- ством и записывает полученные от него данные в буфер. Предположим, что он успел записать только часть данных, а потом был вытеснен. Теперь поток кон- троллера прочтет сообщение, состоящее наполовину из новых и наполовину из старых данных. Это мусор, при обработке которого приложение может «сгореть ярким пламенем». Решение проблемы синхронизации Чтобы избежать порчи данных вследствие контекстного переключения, необ- ходимо синхронизировать доступ к структуре RawData. Но это может оказаться нелегкой задачей. Для правильной работы многопоточное приложение должно применять механизмы синхронизации правильно. На рис. 11.8 представлена архитектура программы, позволяющая решить про- блему синхронизации. Здесь мы видим три потока. Поток в верхней части назван WinMain. Это основ- ной поток программы, создаваемый ядром Windows СЕ. Его программа получает даром. Внизу находятся два потока, выполняющих реальную работу: А и В. Их создает поток WinMain, поэтому они называются его потомками. Помимо самих потоков, на рисунке изображены пунктирными стрелками вза- имодействия между ними. Поток, из которого стрелка исходит, посылает сигнал другому потоку, а тот, в свою очередь, ожидает сигнала от отправителя. Такой метод синхронизации называется «сигнал/ожидание». Приглядимся к этой архитектуре внимательнее. В ней есть шесть объектов синхронизации. Быть может, это число - для вас неожиданность. Большинство программистов полагают, что доступ к структуре RawData контролируется един- ственным объектом синхронизации. Но в данной ситуации все шесть объектов абсолютно необходимы. Рассмотрим совместную работу потоков с применением объектов синхрони- зации. В самом начале поток WinMain создает поток А и поток В. Эти потоки ждут, пока WinMain не пошлет сигнал начать выполнение, пользуясь соответ- ственно объектами StartA и StartB. Затем они начинают работать, a WinMain ждет их завершения. Поток В ждет, когда поток А пошлет сигнал, что он произвел запись в теку- щую позицию массива RawData. Для отправки такого сигнала поток А использует объект AStepCompleted, затем ждет, когда поток В закончит работу с текущей по- зицией RawData, после чего переходит к следующей. Поток В извещает поток А с помощью объекта BStepCompleted.
Введение в проблему синхронизации IIIIHHHMI 299 Создан неявно <Acompleted> <StartA> <StartA> <AStepCompleted> <BStepCompleted> ______Поток_____ FF Объект —Синхронизация Рис. 11.8. Многопоточная архитектура с синхронизацией После того как оба потока завершат работу с массивом RawData, они сообща- ют об этом WinMain. Для этой цели используются объекты ACompleted и BCompleted. Именно этих сигналов поток WinMain и дожидался. Все описанные сигналы необходимы. Поток А должен начать процесс порож- дения и записи данных первым. К сожалению, в документации по Win32 API ни- чего не сказано о порядке запуска потоков. Поэтому и приходится применять объекты синхронизации Start А и StartB, чтобы гарантировать, что А начнет рабо- тать раньше В. С этой целью WinMain сначала посылает сигнал объекту StartA Еще одна проблема - как удержать в памяти основной поток WinMain, пока его потомки не завершатся. этом отношении документация по Win32 API страдает неполнотой. Если поток, исполняемый в функции WinMain, достигнет точки выхода, то ядру разрешается удалить и его самого, и всех его потомков. Нас это не устраивает. Чтобы решить проблему, мы ввели еще два объекта синхрони- зации ACompleted и BCompleted. Поток WinMain сможет завершиться только после получения от них сигналов. Ну а объекты AStepCompleted и BStepCompleted нужны для контроля досту- 'па к разделяемому массиву RawData. Каждый рабочий поток ведет себя как доб- рый сосед. Поток А обновляет данные в текущей позиции и сообщает потоку В, что она свободна. Поток В в изысканности манер не уступает потоку А Некоторые детали проектирования Для синхронизации доступа к данным у программиста есть несколько вариан- тов. Ниже мы познакомимся с рядом специальных компонентов, составляющих
300 1111 Многопоточные приложения и синхронизация каркас контролируемого доступа. И сами компоненты, и порядок их применения показаны на рис. 11.9. Слева находится компонент SynchMgr. Он играет роль менеджера объектов синхронизации. Объекты AStepCompleted и BStepCompleted реализованы как ста- тические паременные, видимые только внутри компонента. Обратиться к ним мож- но лишь с помощью методов доступа, например WaitForAStepCompletedSignal. На рис. 11.9 представлен также компонент DataMgr. Он управляет доступом к структуре RawData. В закрытом поле RawData, находящемся внутри прямо- угольника DataMgr, как раз и хранится разделяемый массив. Добраться до него можно лишь с помощью методов доступа GetRawDataAt и PutRawDataAt. Некоторые разработчики считают, что помещать все объекты синхронизации и методы отправки и ожидания сигнала в отдельный компонент - это чересчур. Но такой подход приносит и дополнительные дивиденды. Рис. 11.9. Реализация безопасного относительно потоков доступа к данным ПРИМЕЧАНИЕ Инкапсуляция всех элементов, относящихся к синхронизации, в один компонент гарантирует строго синхронизированный доступ к внутренним структурам дан- ных. Если за синхронизацию отвечают несколько программистов, велика вероят- ность, что один их них что-то забудет сделать. Компонент DataMgr помогает решить несколько важных задач. Поскольку структура данных RawData должна быть глобальной, он служит хранилищем для
Реализация синхронизированных потоков ЦЦНИНИ 301 нее. Сокрытие структуры и предоставление специальных методов доступа к ней гарантируют атомарность операций. Кроме того, внешняя программа ничего не знает о внутреннем устройстве RawData. В данной реализации RawData - массив, размер которого фиксирован на этапе компиляции. Но если понадобится динами- чески изменять размер, то массив можно будет заменить связанным списком. По- скольку детали реализации скрыты внутри DataMgr, такая модификация не отра- зится на остальной программе. На основе рис. 11.9 можно сделать вывод о том, какая последовательность вызовов необходима для синхронизации доступа к RawData. На псевдокоде она выглядит так: SynchMgr.WaitForACompletedSignal() ; Value = DataManagerElement.GetRawDataAt(Currentindex) ; // Умножить Value на 20 DataManagerElement.GetRawDataAt(Currentindex, Value) ; SynchMgr.SignalStepBCompleted() ; Для любой операции доступа к RawData используются методы, предоставляе- мые компонентом DataMgr. Окружив последовательность манипуляций с разде- ляемыми данными обращениями к методам из SynchMgr, мы гарантируем, что она выполняется атомарно. Доступ к RawData из потока А на все время ее выпол- нения блокирован так же надежно, как если бы пользователь вырубил ток между потоком А и памятью, занимаемой структурой RawData. Реализация синхронизованных потоков В оставшейся части главы мы займемся анализом конкретной реализации описанной выше проблемы синхронизации. Мы рассмотрим ее в следующем по- рядке. 1. Создан ие потоков. 2. Реализация основного потока. 3. Реализация дочерних потоков. 4. Создание объектов синхронизации. 5. Ожидание завершения одного шага. 6. Отправка сигнала о завершении шага. 7. Ожидание завершения дочерних потоков. Именно в таком порядке и выполняются действия в программе. Анализ проводится с точки зрения потока В. Трассировка выполнения с пози- ций потока А привела бы к таким же результатам. Создание потоков ThreadBHandle = CreateThread( 0,0, (LPTHREAD_START_ROUTINE )ThreadBProcedure, 0,0,SThreadBID) ; Для создания нового потока применяется функция Win32 API CreateThread. Ее первый аргумент - адрес функции, в которой поток начинает исполнение,
302 III! Многопоточные приложения и синхронизация в данном случае Thread BProcedure. Все такого рода функции принадлежат ком- поненту ThreadMgr. Объект ThreadBHandle, который возвращает CreateThread, - это описатель потока, созданного ядром Windows СЕ. Реализация потока WinMain WaitForThreadsToSetup)) ; StartThreads() ; WaitForThreads() ; Этот код координирует совместную деятельность основного и рабочих пото- ков с помощью функций, предоставляемых другими компонентами. Функция WaitForThreadsToSetup приостанавливает WinMain до тех пор, пока не придут сигналы о завершении инициализации рабочих потоков. Затем WinMain запуска- ет рабочие потоки, отправляя им сигнал, разрешающий начать работу. Функция StartThreads сначала посылает сигнал потоку А, поэтому он запустится первым. Запустив потоки, WinMain терпеливо ждет их завершения. Ожидание обоих сиг- налов реализовано внутри WaitForThreads. Реализация дочернего потока void ThreadBProcedure(void * Parameter) { int NumberEntries ; int Currentvalue ; int i ; WaitForBStartSignal() ; SignalBStepCompleted)) ; NumberEntries = GetNumberArrayElements() ; for ( i = 0 ; i < NumberEntries ; i = i + 1 ) { WaitForAStepCompletedSignal() ; Currentvalue = GetRawDataAt( i ) ; Currentvalue = 20 * Currentvalue ; PutRawDataAt( i , Currentvalue ) ; SignalBStepCompleted() ; } SignalBCompleted() ; } Точка входа в поток должна иметь вполне определенную сигнатуру. Она воз- вращает void (то есть ничего), а принимает единственный параметр типа void *, то есть указатель на данные произвольного типа. Если программа хочет передать потоку начальные данные, то может поместить их в некоторую структуру и пере- дать указатель на нее. Сразу после входа в функцию исполнение приостанавливается до получения сигнала StartB. Затем поток В в цикле выбирает и обновляет данные, как было описано в псевдокоде выше. Когда все позиции в RawData обновлены, поток сиг- нализирует WinMain о своем завершении.
Реализация дочернего потока 303 iiiibmi Создание объектов синхронизации static HANDLE BStepCompleted ; BStepCompleted = CreateEvent(0,FALSE, FALSE,_TEXT("BStepCompletedEvent") ) ; CloseHandle(BStepCompleted) ; Эти предложения взяты из компонента SynchMgr. Каждое из них играет важ- ную роль в жизненном цикле объекта BStepCompleted. Потоки могут сигнализировать друг другу с помощью объекта Win32 API, на- зываемого событием (event). Объект события может быть создан в одном из двух состояний. В состоянии «свободен» (signaled) объект доступен ожидающему его потоку. Если же объект «занят» (non-signale), то все потоки, ожидающие этого события, становятся к нему в очередь. Есть два вида событий: с ручным и автоматическим сбросом. Поток, захватив- ший событие с ручным сбросом, должен явно вызвать функцию, которая переве- дет его в состояние «занят». Событие же с автоматическим сбросом переходит в это состояние сразу после пробуждения ожидающего его потока без вмешатель- ства программы. В рассматриваемом приложении используются только события с автоматическим сбросом. Для создания события с автоматическим сбросом служит функция Win32 API CreateEvent. Ее первый аргумент задает разрешения для данного объекта. В Windows СЕ этот аргумент всегда равен 0, поскольку эта ОС не поддерживает изощренной системы безопасности, имеющейся в Windows 2000. Второй аргу- мент - признак ручного сброса. Если он равен FALSE, создается событие с автома- тическим сбросом. Третий аргумент - начальное состояние события. FALSE озна- чает, что объект события вначале занят. Любой поток, который попытается захватить этот объект, будет блокирован, пока какой-то другой поток не переведет его в состо- яние «свободен». Последний аргумент - глобальное имя события. Зная это имя, любой поток, даже в другом приложении, сможет получить доступ к событию. Когда все потоки приложения завершатся, функция Win32 API CloseHandle сообщает ядру, что объект синхронизации больше не нужен. В результате умень- шается счетчик ссылок, который инициализируется значением 1 при создании любого объекта синхронизации. С помощью этого счетчика ядро отслеживает те- кущее число пользователей объекта. Когда счетчик достигает нуля, объект унич- тожается. Ожидание завершения шага void WaitForAStepCompletedSignal(void) { DWORD Status ; Status = WaitForSingleObject(AStepCompleted,INFINITE) ; if ( Status != WAIT_OBJECT_0)
304 MHHIHIIIi Многопоточные приложения и синхронизация Для ожидания сигнала от другого потока применяется функция WaitForSingleObject. Первым аргументом ей передается описатель события, ко- торое поток желает захватить, а вторым - максимальное время ожидания. Если второй аргумент равен константе INFINITE, то поток будет ждать события нео- пределенно долго. Если ожидаемое событие сейчас занято, то поток переходит в состояние «ожи- дание» (см. рис. 11.2). Ядро ставит поток в очередь к событию. Инкапсуляция механизма синхронизации позволяет избежать еще одной про- блемы, часто встречающейся в многопоточных программах. Если два потока пы- таются синхронизироваться по двум объектам, но захватывают их в противопо- ложном порядке, то может возникнуть тупиковая ситуация, когда каждый поток ждет события от другого. Скрыв синхронизацию в одном методе, мы можем га- рантировать, что объекты будут захватываться в одном и том же порядке. При этом тупиковой ситуации не возникнет. Отправка сигнала о завершении шага void SignalBStepCompleted(void) { SetEvent(BStepCompleted) ; } Поток, владеющий объектом синхронизации, может освободить его, вызвав функцию SetEvent. Когда объект события переходит в состояние «свободен», ядро выбирает пер- вый поток из стоящей к этому событию очереди и переводит его в состояние «го- тов» (рис. 11.2). Поскольку это событие с автоматическим сбросом, то ядро тут же возвращает его в состояние «занят», но владельцем становится уже другой поток. Ожидание завершения дочерних потоков void WaitForThreads(void) { DWORD Status ; #if (WindowsCE HANDLE ThreadHandles[2] ; ThreadHandles[0] = ACompleted ; ThreadHandles[1] = BCompleted ; Status = WaitForMultipleObjects(2,ThreadHandles,TRUE,INFINITE) ; if ( Status != WAIT_OBJECT_0 ) { } #else Status = WaitForSingleObject(ACompleted,INFINITE) ; if ( Status ! = WAIT_OBJECT 0) { WaitForSingleObject(BCompleted,INFINITE) ;
Резюме iiiiiim 305 if ( Status != WAIT_OBJECT_0) { ) } #endif } Перед выходом из потока WinMain программа ждет поступления сигналов о завершении всех дочерних потоков. Ожидание реализуется по-разному для на- стольного ПК и Pocket PC. Выбор нужной ветви осуществляется на этапе компи- ляции в зависимости от значения константы WindowsCE, определенной в файле IFiles.h. Первая ветвь соответствует версии для настольного ПК. В Win32 API есть простой и удобный механизм ожидания сразу нескольких объектов - функция WaitForMultipleObjects. Чтобы воспользоваться ей, программа заполняет массив описателями объектов и передает его WaitForMultipleObjects вместе с макси- мальным временем ожидания. Третий аргумент, равный в нашем случае TRUE, означает, что ждать нужно освобождения всех объектов. К сожалению, Windows СЕ не поддерживает функции WaitForMultipleObjects. Поэтому приходится записывать серию вложенных предложений if, в каждом из которых вызывается функция WaitForSingleObject. Возврат из каждой функции говорит о том, что очередной рабочий поток завершился. Резюме В этой главе мы познакомились с организацией потоков и синхронизации в Windows СЕ. Вот что следует запомнить: □ потоки совместно используют процессор и позволяют одновременно об- служивать несколько интерфейсов; □ контекстное переключение может привести к порче разделяемых данных; □ сигналы позволяют синхронизировать доступ к разделяемым данным со стороны нескольких потоков; □ каждое приложение автоматически получает один основной поток; □ потоки могут обмениваться сигналами; □ ядро Windows СЕ не поддерживает функцию WaitForMultipleObjects; □ сокрытие объектов синхронизации в инкапсулированном компоненте га- рантирует правильную и надежную работу с ними. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание ________________________________________Папка Прыгающие квадратики для настольного ПК BouncingSquareProgram Прыгающие квадратики для Pocket PC BouncingSquareProgramPPC Программа синхронизации для настольного ПК Synchronizationprogram Программа синхронизации для Pocket PC SynchronizationProgramPPC
306 3MBIIIII Многопоточные приложения и синхронизация Инструкции по сборке и запуску Прыгающие квадратики для настольного ПК 1. Запустите Visual C++6.0. 2. Откройте проект BouncingSquareProgram.dsw в папке BouncingSquare- Program. 3. Соберите программу. 4. Запустите программу. 5. Выберите из меню Priority пункт Below Normal. 6. Несколько раз щелкните левой кнопкой мыши в любой точке клиентской области. В результате начнется вертикальное перемещение квадратиков. 7. Выберите из меню Priority пункт Normal. 8. Щелкните левой кнопкой мыши в любой точке клиентской области. Будет создан новый прыгающий квадратик с более высоким приоритетом, чем у созданных ранее. Но через некоторое время начальный приоритет Normal понизится, и квадратики с приоритетом Below Normal снова начнут полу- чать больше процессорного времени. 9. Выберите пункт меню Quit. 10. Окно закроется, так как приложение завершило работу. Прыгающие квадратики для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект BouncingSquareProgramPPC.vcw в папке BouncingSquare- ProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу BouncingSquareProgram. 12. Выберите из меню Priority пункт Below Normal. 13. Несколько раз коснитесь стилосом любой точки клиентской области. В результате начнется вертикальное перемещение квадратиков. 14. Выберите из меню Priority пункт Normal. 15. Коснитесь стилосом любой точки клиентской области. Будет создан но- вый прыгающий квадратик с более высоким приоритетом, чем у создан- ных ранее. Но через некоторое время начальный приоритет Normal пони- зится, и квадратики с приоритетом Below Normal снова начнут получать больше процессорного времени. 16. Выберите пункт меню Quit. 17. Окно закроется, так как приложение завершило работу.
Примеры программ в Web iiiiiihi 307 Программа синхронизации для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект SynchronizationProgram.dsw в папке Synchronization- Program. 3. Соберите программу. 4. Запустите программу. 5. Щелкните левой кнопкой мыши в любой точке клиентской области. В ре- зультате в клиентской области должны появиться числа, кратные 200. 6. Выберите пункт меню Quit. 7. Окно закроется, так как приложение завершило работу. Программа синхронизации для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект SynchronizationProgramPPC.vcw в папке Synchronization- ProgramPPC. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу Synchronizationprogram. 12. Коснитесь стилосом любой точки клиентской области. В результате в клиентской области должны появиться числа, кратные 200. 13. Выберите пункт меню Quit. 14. Окно закроется, так как приложение завершило работу.
Глава 12. Использование COM-объектов Программисты хотят, чтобы разработанное ими программное обеспечение было пригодно для повторного использования. В этом случае можно быстро со- здавать новые продукты. А коль скоро для их создания применяются уже готовые, тщательно протестированные компоненты, то надежность повышается. В линейке продуктов Windows основной технологией разработки повторно используемого программного обеспечения является модель компонентных объек- тов (СОМ) и ActiveX. Вообще говоря, идеология СОМ применима к компонен- там, не имеющим графического интерфейса. Но дополняющая ее технология ActiveX позволяет включить в COM-объект визуальные элементы, обеспечиваю- щие взаимодействие с пользователем. Считать, что COM-объекты отличаются от элементов управления ActiveX только наличием визуальных черт, было бы упрощением. На самом деле элемен- ты ActiveX тоже являются СОМ-объектами. Однако для данной главы это искусственное разделение позволит выявить отличие в программировании визуальных и невизуальных компонентов. Сначала мы дадим концептуальное введение в технологию СОМ. Этих сведе- ний будет достаточно для понимания заключительной части главы, где описыва- ются детали реализации. ПРИМЕЧАНИЕ Весь код в этой главе написан на C++. Хотя COM-объекты и можно реализовать на ANSI С, но это не рекомендуется. Реализации, написанные на С, получаются очень запутанными и трудными для отладки. C++ - естественный язык для со- здания COM-объектов, поскольку в него встроен тот вид поддержки указателей на функции, который необходим для реализации COM-интерфейсов. Впрочем, трудные аспекты C++ скрыты за набором шаблонов, так что большинство про- граммистов их никогда и не увидят. Модель компонентных объектов СОМ-объект - это двоичный компонент, который можно использовать в лю- бой программе. Основное достоинство COM-объектов в том, что их можно вклю- чать в программу независимо от места физического расположения, к тому же динамически во время выполнения. Динамическое связывание отличается от стандартного способа повторного использования, с которым программисты хоро- шо знакомы. Как правило, чтобы использовать существующий код, программа
Модель компонентных объектов 309 статически связывается с библиотекой на этапе компиляции. В этом случае объект становится неотъемлемой и постоянной частью приложения. При динами- ческом связывании программа подключает объект уже после запуска. Чтобы динамически связать двоичный объект, его нужно сначала найти. В этом отношении программа действует в кооперации с операционной системой, соблюдая правила, определенные в модели СОМ. На рис. 12.1 изображены все элементы, принимающие участие в процессе ди- намического связывания объекта. Каждый COM-объект на устройстве Pocket PC должен быть зарегистрирован в операционной системе. Регистрационная информация хранится в реестре Windows СЕ. COM-объекту назначается идентификатор класса (ClassID), уни- кальный во времени и в пространстве. В литературе он называется глобально уни- кальным идентификатором (GUID). Программа, желающая динамически загрузить зарегистрированный СОМ- объект, должна знать его ClassID. В Windows СЕ имеется специальный компо- нент, который по идентификатору класса отыскивает объект. Он называется Service Control Manager (SCM - диспетчер сервисов). Получая ClassID, SCM ищет соответствующий ему COM-объект в специаль- ной области реестра, а именно в улье HKEY_CLASSES_ROOT. Улей представля- ет собой хранилище данных, принадлежащих одной широкой категории. В записи реестра для указанного ClassID хранится путь к динамически загружаемой биб- лиотеке, которая и представляет собой физическое воплощение СОМ-объекта. SCM загружает эту библиотеку и возвращает указатель на один из интерфейсов объекта, с помощью которого можно получить указатели на реализованные объектом методы. Обычно программа взаимодействует с объектом, выполняя методы, к кото- рым получает доступ через указатель на интерфейс. Если COM-объекту нужно организовать двустороннее взаимодействие с клиентской программой, та должна зарегистрировать обработчики событий. При возникновении определенных усло- вий COM-объект будет извещать зарегистрированного обработчика о событиях. ClassID Реестр Создать объект | Клиент |< Метод Servise Control Manager Найти и загрузить файл, содержащий СОМ-объект ♦(Элемент управления ActiveX| События Двоичный код Рис. 12.1. Элементы СОМ, участвующие в динамическом связывании
310 MMIIIII Использование COM-объектов Чтобы процедура динамического связывания работала правильно, СОМ- объект, или, как его еще называют, сервер, должен удовлетворять некоторым тре- бованиям. С COM-объектом взаимодействуют три элемента. Клиентское прило- жение выступает в роли потребителя предоставляемых объектом сервисов. Доступ к объекту не должен зависеть от того, на каком языке написано клиент- ское приложение. И наконец, операционная система в лице SCM требует, чтобы информация о COM-объекте была помещена в реестр. На рис. 12.2 представлены различные аспекты COM-объекта и взаимосвязи между ними. С точки зрения прикладного программиста COM-объект раскрывает некото- рые интерфейсы (например, ISimpleCOMServer), в которых определены один или несколько методов (например, DisplayString). Взгляд на COM-объект как на набор интерфейсов дает ряд важных преимуществ. Программист применяет еди- нообразные методы доступа, что сокращает время обучения. Кроме того, програм- мисту не нужно знать, где физически находится COM-объект, он может быть даже на другой машине. Детали установления и разрыва соединения с другой ма- шиной скрыты за интерфейсом объекта. Для реализации доступа к COM-объекту язык программирования должен предоставлять определенные средства. Доступ к интерфейсам и методам произво- дится через таблицу указателей на функции. На рис. 12.2 эта таблица представлена стрелкой, исходящей из прямоугольника «Язык программирования». По существу, таблица указателей на функции состоит из целых чисел. Такое представление мало зависит от компилятора, и потому технологию СОМ можно встроить в любой язык программирования. Интерфейс ISimpleCOMServer 1ерациойЖя систем^ Системный реестр Класс SimpleCOMServer Не зависит от языка программирования Рис. 12.2. Различные аспекты СОМ-объекта Когда программа запрашивает COM-объект, SCM ищет его, используя храня- щиеся в реестре данные. Из всей показанной на рис. 12.2 информации наиболь-
Модель компонентных объектов 311 ший интерес для SCM представляет строковое имя СОМ-объекта - ProgramID. С этим именем ассоциирован глобально уникальный идентификатор (GUID), ко- торый отличает данный объект от всех остальных, существующих не только на конкретном компьютере, но и во всем мире. Кроме ProgramID, SCM должен знать тип сервера и физическое местонахождение двоичного кода. Эта информация хранится в ключе реестра InProcServer32. COM-объект предоставляет доступ к своим сервисам через интерфейсы. Ин- терфейс - это просто набор названий методов. Получая указатель на интерфейс, программа, по сути дела, получает указатель на таблицу указателей на функции. А каждый элемент этой таблицы указывает на реализацию метода, объявленного в интерфейсе. Код клиентской программы манипулирует указателями в вышеупомянутой таблице. На какую именно функцию ведет указатель, определяется в момент за- грузки СОМ-объекта. На рис. 12.3 показан простой COM-объект SimpleCOMServer, представлен- ный в виде интерфейса. Этот объект предоставляет единственный метод DisplayString. Внутри объек- та инкапсулирован закрытый член данных ServerCount, в котором хранится чис- ло использующих его в данный момент клиентов. Чтобы сделать этот COM-объект и его единственный метод доступным клиент- ским приложениям, реализация содержит класс SimpleCOMServer и определяет интерфейс ISimpleCOMServer, который содержит указатель на таблицу всего с од- ним элементом - указателем на метод DisplayString. Интерфейс: набор методов, предоставляемых сервером Неявно: содержит только открытые методы Может поддерживать несколько интерфейсов Явно: список всех методов Может содержать открытые методы и члены данных Рис. 12.3. Простой COM-объект, представленный в виде интерфейса
312 Использование СОМ-объектов II» Согласно рис. 12.3, для СОМ-объектана самом деле нужно два GUID’a. Один из них - CLSID, или идентификатор класса, - отличает данный COM-объект от всех остальных. Второй - идентификатор интерфейса, IID, - однозначно опреде- ляет интерфейсы, поддерживаемые данным COM-объектом. По соглашению име- на GUID’ob начинаются либо со слова CLSID, либо IID. Поэтому, глядя на текст программы, всегда можно сказать, имеется в виду COM-объект в целом или один из его интерфейсов. В большинстве книг, посвященных СОМ, интерфейсы СОМ-объектов изоб- ражаются, как показано на рис. 12.3. Скругленным прямоугольником представ- лен сам COM-объект, а исходящими линиями с кружочками на конце - его интер- фейсы. Подобная нотация используется в электрических схемах для обозначения соединений. Представление COM-объекта в виде одного лишь набора интерфейсов под- черкивает его роль в разработке программного обеспечения. Если присоединить клиента к интерфейсу, то мы получим ясную картину того, как устроено приложе- ние. А вообразите, какая неразбериха воцарилась бы, если бы мы стали показы- вать на диаграмме проекта взаимосвязи на уровне методов. Из рис. 12.3 можно сделать несколько выводов. COM-объект может поддер- живать более одного интерфейса. В этом случае каждый интерфейс группирует логически связанные методы, что упрощает работу с COM-объектом. Кроме того, поскольку интерфейс реализован в виде таблицы указателей на функции, то он может предоставлять только методы, но не данные. Следовательно, все данные- члены должны быть закрыты, а доступ к ним возможен лишь с помощью методов. Таким образом, COM-объект скрывает все внутренние структуры данных от клиентского приложения. Если внутреннее представление изменится, модифици- ровать клиентское приложение не придется. Любой COM-объект должен предоставлять как минимум два интерфейса. Один из них определен Microsoft и служит для управления временем жизни COM-объекта. Он называется lUknown. Считайте, что это стандартный интер- фейс, поскольку все COM-объекты должны реализовывать его методы. Помимо него, необходим еще хотя бы один интерфейс, содержащий методы, которые со- ставляют собственно функциональность данного СОМ-объекта. Описание методов интерфейса lUknown приведено на рис. 12.4. Их всего три. Метод Queryinterface позволяет получить указатель на любой из интерфейсов, поддерживаемых данным COM-объектом. Иными словами, с помощью Queryin- terface клиентская программа может преобразовать указатель на один интерфейс в указатель на другой. Можно сказать, что Queryinterface играет роль оператора преобразования типов. Внутри СОМ-объекта инкапсулирован счетчик ссылок, который отслеживает число работающих с ним клиентов. Методы AddRef и Release манипулируют этим счетчиком. Когда счетчик ссылок оказывается равным нулю, COM-объект унич- тожается. По определению любой COM-объект должен реализовать интерфейс IUnknown, в противном случае приложение не сможет с ним рабо - ^ь. Без метода
Модель компонентных объектов ННЯМНИ 313 Query Interface клиентская программа должна была бы загружать несколько ко- пий COM-объекта, что не лучшим образом сказалось бы на производительности. А методы AddRef и Release гарантируют, что объект не уничтожит себя, пока име- ются работающие с ним клиенты. IUnknown гг---- ------1 Возвращает указатель на любой интерфейс, | Query тепа е| ► ПОддерЖИВаемый объектом * Регистрирует нового клиента интерфейса Вызывается, когда у клиента отпадает надобность в данном интерфейсе Рис. 12.4. Стандартный интерфейс IUnknown Каждый COM-объект и все его интерфейсы получают уникальные GUID’bi. Как уже отмечалось, они никогда не повторяются. Структура GUID’a показана на рис. 12.5. Существуют два формата представ- ления GUID’a. В реестре он хранится в виде строки, а в клиентских приложениях и внутри самой реализации COM-объекта представляется в виде структуры данных. Как видно из рис. 12.5, в двоичном виде GUID занимает 128 битов и состоит из четырех полей. При работе с двоичным представлением GUID внутри COM-объекта исполь- зуются некоторая переменная и макрос. Макрос DEFINE_GUID конструирует GUID из отдельных числовых компонентов. Он определен в заголовочном файле initguid.h. Имя переменной обычно начинается с префикса CLSID или IID в зави- симости от того, представляет ли она идентификатор класса или интерфейса. Для генерирования GUID’ob в Embedded Visual Studio имеется специальный инструмент. Желательно, чтобы компьютер, на котором генерируются GUID’bi, был оборудован сетевой картой, иначе стопроцентную уникальность не удастся гарантировать. Когда COM-объект устанавливается на компьютер, в реестр заносится инфор- мация о нем. Мы уже немного говорили о ней, но тогда обсуждение было неполным. На рис. 12.6 показано, какие данные требуются для описания типичного СОМ-сервера. Реестр Windows СЕ представляет собой совокупность крупных разделов, на- зываемых ульями (hive). В улье HKEY_CLASSES_ROOT хранится информация обо всех зарегистрированных COM-объектах. Улей организован иерархически, примерно как файловая система, состоящая из каталогов и файлов. Как и в фай- ловой системе, для доступа к конкретной записи реестра нужно указать путь.
Использование COM-объектов 314 | Величина, однозначно определяющая объект во времени и в пространстве Строковый формат //{0AF25F00-387D-11d3-9D31 -00А0СС39621А} DEFINE_GUID(CLSID_SimpleCOMServer, 0xaf25f00, 0x387d, 0x11d3, 0x9d, 0x31,0x0, OxaO, Oxcc, 0x39,0x62, 0x1 a}; Макрос, строящий двоичное представление из отдельных чисел, определен в файле <initguid.h> Двоичный формат Определена в <basetyps.h> Рис. 12.5. Структура глобально уникального идентификатора (GUID) Рис. 12.6. Регистрация СОМ-объекта
Модель компонентных объектов IIIHM 315 COM-объект регистрируется в двух разных частях реестра, но обе принад- лежат улью HKEY_CLASSES_ROOT. На самом верхнем уровне улья находится строковое имя объекта - не зависящий от версии идентификатор программы. Со строковым именем ассоциирован подключ, содержащий CLSID объекта в стро- ковом формате. Путь к записи о COM-объекте, рассматриваемом в этой главе, имеет вид: HKEY_CLASSES_ROOT\SimpleCOMServer\CLSID = {0AF25F00-387D-11D3-9D31-00A0CC39621A) С помощью функции CLSIDFromProgID клиентская программа преобразует строковое имя SimpleCOMServer в соответствующий ему GUID. Она позволяет получить GUID, не указывая его двоичное представление в программе. Другая часть регистрационной информации располагается ниже ключа CLSID и позволяет SCM найти и загрузить COM-объект в память. Объект дол- жен создать подключ, совпадающий со строковым представлением его GUID’a, а под ним еще три подключа: ProgID, VersionIndependentProgID и InprocServer32. Самым важным из всех является подключ InprocServer32. Его значение - это пол- ный путь к файлу, в котором находится двоичный код СОМ-объекта, например: HKEY_CLASSES_ROOT\CLASID\ {OAF25FOO-387D-11D3-9D31-OOAOCC39621A}\InprocServer32 = с:\SimpleCOMServerCPP\SimpleCOMServerCPP-dll В данном случае файл реализации представляет собой DLL. Тогда таблица указателей на функции инициализируется в процессе динамической загрузки, а не статической компоновки. Кроме того, разные клиенты могут пользоваться одной копией библиотеки, что позволяет более экономно расходовать ресурсы ма- шины. Получив GUID с помощью функции CLSIDFromProgID, клиентское прило- жение передает его SCM, чтобы тот загрузил COM-объект и вернул указатель на интерфейс. Для этого предназначена функция CoCreatelnstance. Первым делом она формирует полный путь, показанный выше, а затем считывает из реестра путь к файлу, в котором находится код СОМ-объекта. Подробная диаграмма работы CoCreatelnstance показана на рис. 12.7. В верх- ней ее части показаны программные элементы, принимающие участие в создании СОМ-объекта, а сверху вниз идет последовательность взаимодействий между ними. Клиентское приложение вызывает функцию CoCreatelnstance, чтобы полу- чить указатель на интерфейс, поддерживаемый COM-объектом. Эта функция об- ращается к диспетчеру сервисов SCM с просьбой загрузить COM-объект. SCM ищет в реестре запись по указанному пути и считывает из нее путь к динамически загружаемой библиотеке (DLL). Эту информацию он передает операционной си- стеме Windows СЕ с помощью функции LoadLibrary. Система загружает библио- теку и возвращает ее описатель. Зная описатель, SCM получает указатель на экс- портируемую из библиотеки функцию DLLGetClassObject. Эта функция должна присутствовать в любой библиотеке, реализующей COM-объект. Затем SCM вы- зывает эту функцию по указателю.
316 MBIIII Использование COM-объектов Рис. 12.7. Последовательность создания СОМ-объекта Функция DLLGetClassObject создает объект фабрики классов, который также входит в состав инфраструктуры СОМ. Указатель на интерфейс фабрики классов IClassFactory * возвращается по цепочке SCM. Получив этот указатель, SCM вы- зывает метод фабрики классов Createlnstance, который и создает экземпляр клас- са, реализующего все методы, необходимые клиентскому приложению. В процес- се создания объекта класса заполняется таблица указателей на функции, так что клиент сможет вызывать методы через интерфейс. SCM получает в качестве воз- вращаемого значения указатель на стандартный интерфейс IUnknown *. Получив указатель на этот интерфейс, SCM вызывает метод Release фабрики классов. В результате она выгружается из памяти, оставляя сам COM-объект. Избавив- шись от фабрики классов, SCM возвращает указатель на стандартный интерфейс клиентской программе. Он ведет на таблицу указателей на поддерживаемые объектом методы, так что клиент может обращаться к ним. Целиком эта последовательность взаимодействий выполняется лишь при первом создании СОМ-объекта клиентом. Другой клиент, пожелавший восполь- зоваться методами того же СОМ-объекта, получит тот же самый указатель на ин- терфейс. Когда COM-объект реализован в виде DLL, к нему могут одновременно обращаться несколько клиентов, поскольку Windows автоматически поддержива- ет разделение DLL. COM-объект может исполняться в разных контекстах в зависимости от того, где находится файл с его кодом. Вариантов три: внутрипроцессный сервер,
Модель компонентных объектов IIIIIHHIM 317 локальный сервер и удаленный сервер. На рис. 12.8 охарактеризованы все три случая. Внутрипроцессный сервер выполняется в адресном пространстве клиентско- го приложения, поэтому его производительность максимальна. Такие СОМ- объекты программировать проще, так как приходится писать меньше кода для интеграции с окружающей средой. Но при этом приходится регистрировать объект на каждой машине, на которой может работать клиентское приложение. Машина А Процесс С Процесс D Клиент СОМ-Объект СОМ-объект Удаленный сервер Внутрипроцессный Локальный сервер сервер Наименьшее время отклика. Наименьшее время разработки. Поддерживает локальные обращения Наибольшее время отклика. Наибольшее время разработки. Поддерживает удаленные обращения Рис. 12.8. Контексты исполнения СОМ-объектов Локальный сервер находится на той же машине, что и клиентское приложе- ние, но исполняется в другом адресном пространстве. Поэтому необходим допол- нительный код для передачи методам аргументов через границы процесса. Эта процедура называется маршалингом интерфейсов. Поскольку на маршалинг ухо- дит время, локальный сервер работает медленнее. А на разработку соответствую- щего кода нужно потратить дополнительные усилия. Так как и в этом случае COM-объект исполняется на той же машине, что и клиент, то регистрировать его надо всюду, где установлено клиентское приложение. И наконец, COM-объект может исполняться на другой машине. При этом клиентская программа также должна выполнять маршалинг аргументов при каж- дом вызове метода. К тому же данные в этом случае передаются по сети. Ясно, что время реакции сервера многократно возрастет и, вообще говоря, не детерминиро- вано. При каждом вызове удаленного метода задержка оказывается различной. Если сеть перегружена, то ждать результата можно довольно долго. Код, реализую- щий маршалинг аргументов для локального сервера, будет работать и для удален- ного. Но отладку придется вести на нескольких машинах, что резко увеличивает
318 MBIIII Использование COM-объектов ее продолжительность. Зато необходимо зарегистрировать единственную копию удаленного COM-сервера, поэтому процедура его обновления оказывается отно- сительно безболезненной. На самом деле маршалинг данных между клиентом и сервером - основной воп- рос при реализациии СОМ-объекта. В зависимости от их взаимного расположения программа применяет различные технологии для передачи аргументов методов. В табл. 12.1 показано, как может осуществляться маршалинг данных и какие границы при этом приходится пересекать. СОМ-объекта выступающий в роли внутрипроцессного сервера, реализуется в виде DLL. Все его методы представлены точками входа в таблицу указателей на функции, размещенную в адресном пространстве клиента. А в таком случае для передачи аргументов нет необходимости в каких-то специальных приемах, мар- шалинг как таковой отсутствует. Если же сервер является локальным или удаленным, то для передачи аргу- ментов нужно пересечь границы процесса или узла сети. Тогда применяются стандартные механизмы маршалинга. На языке определения интерфейсов (IDL — Interface Definition Language) описываются типы аргументов, и компилятор гене- рирует код заглушки и заместителя, в совокупности выполняющих маршалинг. Заместитель включается в клиентское приложение, а заглушка - в СОМ-объект. Таблица 12.1. Технологии маршалинга для передачи данных СОМ-объекту Тип маршалинга Границы Технология Маршалинг отсутствует библиотека Стандартный маршалинг Диспетчеризация Нестандартный маршалинг Динамически загружаемая Процесс/узел сети Язык программирования Процес Глобальная адресация ЯзыкIDL Маршалер автоматизации Специальный протокол Для клиентских приложений, написанных на других языках, например на Visual Basic, применяется иной подход к маршалингу, нежели в C++. Диспетчери- зация - это способ передачи аргументов через границы языка программирования. Предопределенный маршалер автоматизации входит в саму систему Windows СЕ. Чтобы им воспользоваться, и клиентское приложение, и СОМ-объект долж- ны представлять данные в виде стандартной структуры VARIANT. У этого марша- лера имеется единственная точка входа Invoke, которой передается числовой код метода, и его входные аргументы в формате VARIANT. Каждый СОМ-объект, под- держивающий диспетчеризацию, должен реализовать эту точку входа, в коде ко- торой данные извлекаются из VARIANT и передаются вызванному методу. Создание СОМ-объектов с помощью библиотеки ATL Значительная часть кода СОМ-объекта может быть унифицирована. В состав Visual Studio входит библиотека ActiveX Template Library (ATL), в которой уже
Создание COM-объектов ИНОМ 31? реализованы наиболее утомительные детали создания COM-объектов. Стандарт- ный код генерируется с помощью шаблонов, а разработчик может сосредоточить- ся на написании тех методов, которые и составляют специфику приложения. В дополнение к библиотеке шаблонов Embedded Visual Studio предоставляет ряд мастеров, которые делают работу с ней совсем простым делом. Впрочем, в ATL есть несколько скрытых подводных камней, которые мы и обсудим ниже. ------я_ ”| ПРЕДОСТЕРЕЖЕНИЕ *jy V П° большей части, программист при разработке COM-объекта просто пользуется ЕкиУх встроенными в Visual Studio мастерами. Но иногда возникающие ошибки компи- —Д—ж ляции заставляют заглянуть внутрь кода библиотеки. Поэтому знакомство с шаб- лонами C++ (или с тем, кто в них разбирается) будет нелишним. Иначе отладка может оказаться весьма сложным делом, сулящим немало разочарований. Для разработки COM-объекта в Embedded Visual Studio с помощью библиоте- ки ATL выполните следующие действия. 1. Создайте COM-объект с помощью мастера ATL COM AppWizard. 2. Вставьте новый объект с помощью мастера ATL Object Wizard. 3. Добавьте методы объекта с помощью мастера Add Method to Interface Wizard. 4. Напишите код методов объекта. Ниже мы рассмотрим эти шаги в применении к разработке простого СОМ- объекта COMServerProgram, который будет поддерживать единственный интер- фейс ICalculatorMgr. Этот интерфейс предоставляет ряд методов для выполне- ния операций над скрытым регистром: установки, очистки, сложения, вычитания, умножения и деления. Создание СОМ-объекга с помощью мастера ATL СОМ AppWizard . Сначала нужно создать COM-объект, который будет выступать в роли серве- ра. Для этого: 1. В меню File выберите пункт New (Новый). Перейдите на вкладку Projects; 2. Слева находится список типов проектов. Выберите строку ATL СОМ AppWizard; 3. В поле Project name введите имя проекта COMServerprogram; 4. Нажмите кнопку ОК (рис. 12.9); 5. На первом шаге мастера укажите, что хотите создать Dynamic Link Library (DLL); 6. Нажмите кнопку Finish (рис. 12.10); 7. Проверив правильность задания параметров, нажмите кнопку ОК (рис. 12.11). В результате будет подготовлен проект для создания внутрипроцессного сер- вера в виде DLL. В него уже включен весь код, необходимый для регистрации COM-объекта в реестре. Такие объекты регистрируют себя сами.
320 ЯНН Использование СОМ-объектов Выберите строку ATL COM AppWizard Введите имя проекта Перейдите на вкладку Projects Нажмите кнопку ОК Рис. 12.9. Заполненное окно выбора нового проекта 6. Нажмите кнопку Finish 5. Выберите внутрипроцессный сервер Рис. 12.10. Заполненное окно мастера ATL COM AppWizard
Создание СОМ-объектов iiiibhm 321 Проверьте параметры Рис. 12.11. Окно New Project Information, содержащее информацию о новом проекте Нажмите кнопку ОК Вставка нового объекта с помощью мастера ATL Object Wizard После того как обвязочный код сгенерирован, можно приступать к вставке соб- ственно СОМ-объекта. Для этого служит мастер ATL Object Wizard. 1. В меню Insert (Вставка) выберите пункт New ATL Object. 2. Слева находится список категорий. Выберите из него строку Objects. 3. Справа появится список поддерживаемых мастером объектов. Щелкните по иконке Simple Object (Простой объект). 4. Нажмите кнопку Next (Далее) (рис. 12.12). 5. Появится окно ATL Object Wizard Properties (Свойства объекта). Перей- дите на вкладку Names (Имена). 6. В поле Short Name введите имя класса сервера CalculatorMgr. Остальные поля автоматически обновятся. 7. Перейдите на вкладку Attributes (Атрибуты) (рис. 12.13). 8. Задайте атрибуты следующим образом: □ переключатель Threading Model (Потоковая модель) установите в положе- ние Single (Один поток);
322 МНИ Использование СОМ-объектов ATL Object Wizard НЕЗ 2. Выберите Object category Category , Controls Misceflaneous Data Access 3. Выберите Simple Object Qbiects ActiveX Server MMCSrwpIn MS Component T ransacti |: — Lance) | 4. Нажмите кнопку Next □Ы*гЪ- Рис. 12.12. Окно мастера ATL Object Wizard,в котором выбраны категория и вид объекта □ переключатель Interface (Интерфейс) установите в положение Custom (Специальный); □ переключатель Aggregation (Агрегирование) установите в положение No (Нет). Остальные элементы оставьте без изменения. Окно должно выглядеть, как показано на рис. 12.14. 9. В меню View (Вид) выберите пункт Workspace (Рабочее пространство) и перейдите на вкладку Class View (рис. 12.15). 6. Введите короткое Перейдите на имя класса вкладку Attributes Рис. 12.13. Окно свойств мастера ATL Object Wizard с раскрытой вкладкой Names
Создание COM-объектов НИМ I | 323 Теперь в COM-сервере имеются класс CCalculatorMgr и интерфейс ICalculatorMgr. Вместе с классом сервер получает шаблонную версию фабрики классов, которая нужна для создания СОМ-объекта при первой загрузке DLL. Кроме того, сервер наследует реализацию методов интерфейса IUnknown: Queryinterface, AddRef и Release, - которые управляют доступом к поддерживае- мым интерфейсам и временем жизни объекта. Выберите потоковую модель Single Выберите вид интерфейса Custom Нажмите кнопку ОК 1 Укажите, что агрегирование не поддержи- вается Рис. 12.14. Окно свойств мастера ATL Object Wizard с раскрытой вкладкой Attributes Class view in — Project Explorer Пока у клиента еще нет методов Рис 12.15. Окно Project Workspace с раскрытой вкладкой Class View
Использование COM-объектов 324 Добавление методов объекта с помощью мастера Add Method to Interface Wizard После того как окружение СОМ-объекта определено, фабрика классов и интер- фейс IUnknown реализованы, можно добавить методы, составляющие специфику данного объекта. Именно ради них СОМ-объект и создается. Для добавления мето- дов служит мастер Add Method to Interface Wizard. 1. Находясь на вкладке Class View в окне Project Wbkrspace, щелкните пра- вой кнопкой по интерфейсу ICalculatorMgr. 2. В контекстном меню выберите пункт Add Method (Добавить метод). 3. В списке Return Туре (Тип возвращаемого значения) выберите значение Н RESULT. 4. В поле Method Name (Имя метода) введите Add. 5. В поле Parameters (Параметры) введите такую строку (рис. 12.16): [in] double Argument Как правило, методы COM-объектов возвращают значение типа HRESULT При определении параметров метода нужно придерживаться синтаксиса языка IDL. В нем, в частности, указывается направление параметра. Конструкция [in] озна- чает, что это входной параметр, который не будет модифицироваться методом. В поле Implementation мастер покажет полную сигнатуру метода, соответ- ствующую введенным параметрам. 6. Нажмите кнопку ОК. Рис. 12.16. Окно мастера Add Method to Interface Wizard, в котором определен метод Add Новый метод мастер поместит в файл проекта, содержащий описание интер- фейса на языке IDL. Кроме того, будет сгенерирована заглушка метода в классе CCalculatorMgr.
Создание СОМ-объектов И1ИП1 Реализация методов объекта После того как методы СОМ-объекта объявлены, следует написать их код. Чтобы перейти в окно редактора кода, выполните следующие действия. 1. В меню View выберите пункт Workspace и перейдите на вкладку Class View. 2. Щелкните по значку «+» слева от узла CCalculatorMgr, чтобы раскрыть его. 3. Щелкните по значку «+» слева от узла ICalculatorMgr (рис. 12.17). 3 COMServerPiogram clai*e* B-R5 CCalculatorMgr • В ICalcdatoiMgr I ’ ф Addfdouble Argument) < I - - ф CCaJcuiatorMgrQ ffl-QGIcbals New method exposed to the client ICalculakiMqr Рис. 12.17. Вкладка Class View, на которой раскрыт узел, содержащий методы СОМ-объекта 4. Дважды щелкните по методу Add в раскрытом узле ICalculatorMgr. При этом в окно редактора будет загружен текст класса CCalculatorMgr и кур- сор будет установлен в начало метода Add. 5. В тело метода Add добавьте такую строку: Register = Register + Argument ; Этот код обновляет значение закрытого члена Register класса CCalculatorMgr, который объявлен в файле CCalculatorMgr.h (рис. 12.18). 6. В меню Build выберите пункт Build COMServerProgram.dlL Visual Studio выполнит компиляцию и компоновку СОМ-объекта. Если все пройдет успешно, а целевая платформа - настольный ПК, то объект будет еще и зарегистрирован на локальной машине. Теперь клиентские приложения могут им пользоваться. Если же целевой платформой является Pocket PC, то нужно будет сначала загрузить и зарегистрировать объект на Pocket PC. Ниже мы расскажем, как это делается
326 Illi Использование COM-объектов Вставьте код в тело метода Для метода автоматически создана заглушка Рис. 12.18. Вставка кода в тело метода Анализ СОМ-объекга, созданного с помощью ATL В этом разделе мы рассмотрим COM-объект по частям. Большая часть его кода была сгенерирована мастером. Код состоит из объявления класса, тела клас- са, глобальных функций и объектов, файла определения интерфейса и сценария реестра. Объявление класса Объявление класса находится в файле CalculatorMgr.h. Это обычный класс C++ с множественным наследованием: ♦ifndef __CALCULATORMGR_H_ ♦define __CALCULATORMGR_H_ #include «resource.h» // константы // CCalculatorMgr class ATL_NO_VTABLE CCalculatorMgr : public CComObjectRootEx<CComSingleThreadModel>,
Анализ СОМ-объекта пиан 327 public CComCoClass<CCalculatorMgr, &CLSID_CalculatorMgr>, public ICalculatorMgr { public: CCalculatorMgr 0 { Register = 0 ; } DECLARE_REGISTRY_RESOURCEID(IDR_CALCULATORMGR) DECLARE_NOT_AGGREGATABLE(CCalculatorMgr) DECLARE_PROTECT_FINAL_CONSTRUCT () BEGIN_COM_MAP(CCalculatorMgr) COM_INTERFACE_ENTRY(ICalculatorMgr) END_COM_MAP() // ICalculatorMgr public: STDMETHOD(get_Register)(/‘[out, retval]*/ double *pVal); STDMETHOD(put_Register) (/* [in]*/ double newVal); STDMETHOD(Divide)(/‘[in]*/ double Argument); STDMETHOD(Multiply)(/‘[in]*/ double Argument); STDMETHOD(Subtract)(/*[in]*/ double Argument); STDMETHOD(Add)(/‘[in]*/ double Argument); STDMETHOD(Clear) () ; private: double Register ; ); #endif //__CALCULATORMGR_H_ Класс сервера наследует трем базовым классам: CComObjectRootEx, CCom- CoClass, ICalculatorMgr. Два из них являются шаблонными. Параметры шаблона задаются в угловых скобках (<...>) и обозначают классы, на основе которых шаб- лон конкретизируется. Последний базовый класс - это интерфейс, он является абстрактным классом. По правилам языка C++ производный класс должен реали- зовать все методы этого абстрактного класса. Поскольку они и составляют функ- циональность COM-сервера, это требование вполне логично. Класс CComObjectRootEx предоставляет стандартную реализацию методов интерфейса IUnknown: Queryinterface, Add и Release. Коль скоро СОМ-объект наследует этому классу, клиент сможет вызвать метод Queryinterface, чтобы по- лучить указатель на любой поддерживаемый объектом интерфейс. Методы Add Ref и Release отвечают за подсчет ссылок, чтобы удержать объект в памяти, пока у него есть хотя бы один клиент. Базовый класс CComCoClass предоставляет стандартную реализацию фабрики классов. Она создает экземпляр сервера в памяти в процессе загрузки DLL. Для со- здания объекта в шаблон базового класса передается его тип CCalculatorMgr и GUID CLSID_CalculatorMgr. Переменная CLSID_CalculatorMgr содержит двоич- ное представление GUID’a СОМ-объекта. GUID автоматически сгенерировала Embedded Visual Studio.
328 Illi Использование COM-объектов Следующий важный элемент в объявлении класса - карта СОМ. В ней перечис- лены все поддерживаемые этим COM-объектом интерфейсы. Для описания интер- фейса применяется макрос COM_INTERFACE_ENTRY, который после расшире- ния дает элемент таблицы, содержащий GUID интерфейса и указатель на него. В этой таблице метод Queryinterface, унаследованный от класса CComObjectRootEx, будет искать переданный клиентским приложением GUID, и если найдет, то вер- нет соответствующий ему указатель на интерфейс. В конце находятся объявления открытых методов для выполнения операций над скрытым регистром: Add, Subtract, Multiply и Divide. Они следуют принятому в СОМ соглашению о вызове STDMETHOD. Раз методы открыты, к ним сможет обратиться клиентское приложение. В закрытом разделе в конце класса объявлен член Register, которым и манипу- лируют все перечисленные выше методы. В конструкторе класса CCalculatorMgr выполняется инициализация. Определение класса Определение класса CCalculatorMgr находится в файле CalculatorMgr.cpp. Там размещены только методы, реализующие собственно функциональность объек- та, добавленные мастером Add Method to Interface Wizard. Тела всех остальных методов, например Queryinterface, входят в определения шаблонов, предоставляе- мые библиотекой ATL. /*********************************************** ★ * File: CalculatorMgr.cpp ★ * copyright, SWA Engineering, Inc., 2001 * All rights reserved. ★ ***********************************************/ // CalculatorMgr.cpp : Implementation of CCalculatorMgr ♦include "stdafx.h" ♦include "COMServerprogram.h" ♦include "CalculatorMgr.h" //CCalculatorMgr STDMETHODIMP CCalculatorMgr::Add(double Argument) { Register = Register + Argument ; return S_OK; } // Остальной кол опущен Здесь мы видим включение некоторых особенностей СОМ, автоматически учтенных мастером. Перед именем метода стоит слово STDMETHODIMP. Этот макрос определен в заголовочном файле basetyps.h и реализует два важных аспекта. Во-первых, он обеспечивает возврат значения стандартного для СОМ типа HRESULT, а во-вто- рых, сообщает компилятору порядок помещения параметров в стек.
Анализ СОМ-объекта 11111ПМ 329 Поскольку должно быть возвращено значение типа HRESULT, мастер вста- вил строку, возвращающую константу S_OK. Ее структура соответствует форма- ту HRESULT, а сам код означает, что метод завершился успешно. Глобальные функции и объекты На вкладке Class View в окне Workspace есть папка Globals. Раскрыв ее, вы увидите несколько глобальных функций и один глобальный объект. Мастера вставляют эти элементы в проект для поддержки инфраструктуры ATL. Находят- ся они в файле COMServerProgram.cpp. // COMServerProgram.cpp : Implementation of DLL Exports. ♦include "stdafx.h" ♦include "resource.h" ♦include <initguid.h> ♦include "COMServerProgram.h" ♦include "COMServerProgram_i.c" ♦include "CalculatorMgr.h" CComModule _Module; BEGIN_OBJECT_MAP(Obj ectMap) OBJECT_ENTRY(CLSID_CalculatorMgr, CCalculatorMgr) END_OBJECT_MAP() extern "C" BOOL WINAPI DllMain(HINSTANCE hlnstance, DWORD dwReason, LPVOID / *lpReserved*/) { if (dwReason == DLL_PROCESS_ATTACH) { -Module.Init(ObjectMap, hlnstance, SLIBID_COMSERVERPROGRAMLib); DisableThreadLibraryCalls(hlnstance); } else if (dwReason == DLL_PROCESS_DETACH) _Module.Term(); return TRUE; // ok } STDAPI DllCanUnloadNow(void) { return (_Module.GetLockCount()==0) ? S_OK : S_FALSE; } STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { return —Module.GetClassObject(rclsid, riid, ppv); ) STDAPI DllRegisterServer(void) { return -Module.Registerserver(TRUE); ) STDAPI DllUr• (void)
330 Illi Использование COM-объектов { return _Module.UnregisterServer(TRUE) ; } В начале файла находится объявление единственного глобального объекта —Module, принадлежащего классу CComModule. Он реализует методы, необходи- мые для учета всех COM-объектов в данной DLL. Список объектов хранится в таблице ObjectMap. Сервер помещает записи в эту таблицу с помощью макроса OBJECT_ENTRY. Макросу передается два аргумента: GUID, однозначно определяющий СОМ-объект, и имя класса, реализующего этот объект. Имея эту информацию, макрос помеща- ет в таблицу разнообразные сведения, в том числе адреса метода Createlnstance фабрики классов. При загрузке DLL в память SCM вызывает экспортируемую функцию DllGetClassObject, а та обращается к методу Createlnstance фабрики классов и по- лучает от нее указатель на интерфейс. Стандартная реализация DllGetClassObject ре- шает эту задачу, обращаясь к методу GetClassObject глобального объекта _Module. Этот метод ищет в таблице ObjectMap запись, относящуюся к запрошенному COM-объекту, и получает указатель на метод Createlnstance его фабрики классов. Вызвав Createlnstance, метод GetClassObject создает СОМ-объект, вызывает его метод Queryinterface и возвращает SCM полученный указатель на интерфейс. Файл описания интерфейса Файл описания интерфейса на языке IDL служит нескольким целям. Если объект является внепроцессным сервером, то на его основе Visual Studio создает DLL, содержащую пару заглушка / заместитель, необходимую для маршалинга аргументов. Кроме того, по IDL-описанию генерируется библиотека типов (TLB). Она необходима, если интерфейсы СОМ-объекта должны быть доступны про- граммам на Visual Basic. И наконец, компилятор midi, который обрабатывает IDL- файл, порождает заголовочный файл, содержащий GUID’bi COM-объектов и их интерфейсов, а также объявления интерфейсов. Этот файл включается в клиентс- кие приложения, желающие обращаться к СОМ-объекту. Наш сервер является внутрипроцессным, поэтому единственное назначение IDL - сгенерировать заголовочный файл, в котором объявлены GUID’bi и интер- фейсы. Объявление интерфейса порождает таблицу указателей на функции, с по- мощью которой написанные на C++ клиенты могут обращаться к методам интер- фейса. Включая заголовочный файл, клиентское приложение резервирует место для таблицы, а заполнена она будет во время создания СОМ-объекта. import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(3E1DCD2F-4A21-4EDF-BE0E-FE247B5AB317) , pointer_default(unique) interface ICalculatorMgr': IUnknown
Анализ COM-объекта ИН1П 331 { HRESULT Clear О; HRESULT Add([in] double Argument); HRESULT Subtract ( [in] double Argument); HRESULT Multiply([in] double Argument); HRESULT Divide([in] double Argument); [propget] HRESULT Register([out, retval] double *pVal); [propput] HRESULT Register([in] double newVal); }; [ uuid(E677B4AE-937B-40CC-A2B8-6587015722DC), version(1.0) ] library COMSERVERPROGRAMLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [ uuid(20A02FF0-88BD-468B-8C26-286DFA163655) ] coclass CalculatorMgr { [default] interface ICalculatorMgr; }; }; В IDL-файле описаны интерфейсы, коклассы и библиотеки. В описании ин- терфейса перечислены его методы, обычно логически связанные между собой. Кроме того, каждому интерфейсу назначается GUID, который Visual Studio гене- рирует автоматически. COM-объект может поддерживать несколько интерфей- сов, и все они будут описаны в одном IDL-файле. В разделе coclass описаны все интерфейсы, поддерживаемые СОМ-объектом. В сочетании разделы coclass и interface описывают иерархическую структуру объекта. Коклассу также присваивается GUID, который будет затем записан в ре- естр в строковом формате. При наличии раздела library компилятор midi сгенерирует библиотеку типов. Программы на языке VB пользуются ей, чтобы получить доступ к методам объек- та. И у библиотеки типов должен быть GUID, который любезно сгенерирует Visual Studio Сценарий реестра Когда клиентское приложение запрашивает COM-объект, SCM ищет инфор- мацию о нем в реестре. Visual Studio создает сценарий, который позволяет помес- тить в реестр все необходимые сведения. Сценарий реестра для нашего COM-объекта находится в файле CalculatorMgr.rgs: HKCR ( COMServerprogram.CalculatorMgr.1 = s ’CalculatorMgr Class'
332 Illi Использование СОМ-объектов CLSID = s '{20A02FF0-88BD-468B-8C26-286DFA163655} ' } COMServerProgram.CalculatorMgr = s 'CalculatorMgr Class' { CLSID = s '{20A02FF0-88BD-468B-8C26-286DFA163655} ' CurVer = s 'COMServerProgram.CalculatorMgr.!' } NoRemove CLSID { ForceRemove {20A02FF0-88BD-468B-8C26-286DFA163655} = s 'CalculatorMgr Class' { ProgID = s 'COMServerProgram.CalculatorMgr.1' VersionIndependentProgID = s 'COMServerProgram.CalculatorMgr' InprocServer32 = s '%MODULE%' { } 'TypeLib' = s '{E677B4AE-937B-40CC-A2B8-6587015722DC}' } } } Его назначение - поместить данные в улей HKEY_CLASSES_ROOT (HKCR) в ветвь, начинающуюся с ключа CLSID. Под ним создается подключ, равный GUID’y кокласса СОМ-объекта в строковом формате. Внутри этого ключа располагается подключ InprocServer32. Его значение оп- ределяется значением макропеременной %MODULE% в сценарии. Когда сцена- рий обрабатывается системой, вместо нее подставляется имя исполняемого фай- ла, так что в реестре окажется что-то вроде InprocServer32 = "С:\COMServerProgram\debug\COMServerProgram.dll" Поэтому SCM будет знать, где находится файл с исполняемым кодом СОМ- объекта. Создание СОМ-клиента В этом разделе мы опишем СОМ-клиента для простого СОМ-сервера COMServerProgram. Программа будет предоставлять графический интерфейс элементарного калькулятора. В ответ на действия пользователя обработчики со- общений будут обращаться к методам низкоуровневого СОМ-объекта. ПРИМЕЧАНИЕ Во многих программах низкоуровневые COM-объекты используются именно та- ким образом. Типична ситуация, когда COM-объект реализует стандартный ин- терфейс управления оборудованием. Методы объекта обертывают доступ к аппа- ратуре в набор логических операций. Пользовательский интерфейс программы Calculator представлен на рис. 12.19. Данные вводятся в поля, расположенные сверху. Команды, в конечном итоге пе- редаваемые COM-объекту, представлены кнопками в нижней части окна.
Создание COM-клиента 11И1ПН 333 Желая записать значение в регистр, пользователь должен выполнить следую- щие действия: 1. Ввести числовое значение в поле Register. 2. Нажать кнопку Set. После того как регистр инициализирован, можно выполнять над ним различ- ные операции. 1. Ввести числовое значение в поле Argument. 2. Нажать кнопку, соответствующую нужной команде. Интерфейс намеренно сделан максимально простым, чтобы его реализация не отвлекала внимания от кода, необходимого для организации взаимодействия с СОМ-объектом. Нажать кнопку Set 3. Ввести значение аргумента 1. Ввести значение регистра 4. Нажать кнопку, представляющую операцию Рис. 12.19. Графический интерфейс СОМ-клиента ПРИМЕЧАНИЕ _______________________________________________ Клиентская программа в папке COMChentProgram - это стандартное диалоговое приложение, структура которого была разработана в главе 3. Только теперь все исходные файлы имеют расширение .срр. Это необходимо для правильной рабо- ты с таблицей указателей на функции в СОМ-сервере. Получение информации об интерфейсе СОМ-объекта Одним из шагов компиляции COM-сервера была обработка IDL-файла про- граммой midi. Она генерирует ряд важных файлов, два из которых необходимы для того, чтобы клиентское приложение могло получить информацию об интер- фейсах СОМ-объекта и реализованных им методах.
334 Использование COM-объектов Illi Первым делом скопируйте в папку проекта клиентской программы файл COMServerProgram_i.c и COMServerProgram.h. В файле COMServerProgrami.c объявлены GUID’bi самого СОМ-объекта, его интерфейсов и библиотеки типов. Помимо GUID’ob, клиенту еще потребуется таблица для хранения указателей на интерфейсы и методы СОМ-объекта. Необходимые объявления находятся в фай- ле COMServerProgram.h. Программирование доступа к СОМ-объекту через интерфейс Использовать COM-сервер в клиентском приложении не так уж сложно. Про- цедура состоит из следующих шагов. 1. Импортировать необходимые GUID’bi и таблицы указателей на функции. 2. Объявить переменную для хранения указателя на интерфейс. 3. Создать экземпляр СОМ-объекта. 4. Поработать с методами созданного объекта. 5. Уничтожить объект. Ниже на примере фрагментов клиентской программы COM Clientprogram ил- люстрируется каждый шаг. Импорт GUID’ob и таблиц указателей на функции ♦include <initguid.h> ♦include "COMServerProgram_i.с" ♦include "COMServerProgram.h" Включив эти три файла, программа получает всю информацию, необходимую для доступа к СОМ-объекту. В файле initguid.h содержатся макросы для инициа- лизации GUID’ob. В файле COMServerProgram i.c объявлены GUID’bi конкретного СОМ- объекта, его интерфейсов и библиотеки типов. Помимо GUID’ob, программе тре- буется таблица указателей на интерфейсы и методы. Объявление этой таблицы находится в файле COMServerProgram.h. Объявление переменной для хранения указателя на интерфейс static ICalculatorMgr * Calculator ; Переменная Calculator, объявленная как указатель на интерфейс ICalcula- torMgr, на самом деле указывает на таблицу указателей на методы объекта COMServerProgram. Эта таблица будет заполнена после загрузки СОМ-объекта в память. Создание экземпляра СОМ-объекта ♦if IWindowsCE Coinitialize(NULL) ; ♦endif CoCreatelnstance(CLSID_CalculatorMgr, NULL,
Создание COM-клиента IIIMH 335 CLSCTX_INPROC_SERVER, IID_ICalculatorMgr,(void **)&Calculator) ; Для создания COM-объекта нужно выполнить два шага. Если программа за- пускается на настольном ПК, то сначала нужно инициализировать подсистему СОМ, для чего служит функция Coinitialize. В случае Pocket PC этот шаг излишен. Затем с помощью функции Win32 API CoCreatelnstance создается экземпляр COM-объекта. Первым аргументом ей передается GUID кокласса CLSID_ Cal- culatorMgr. Второй аргумент для большинства программ для Pocket PC равен NULL. Константа CLSCTX_INPROC_SERVER говорит, что COM-объект будет выступать в роли внутрипроцессного сервера, поэтому SCM должен искать в рее- стре ключ InprocServer32, в котором хранится путь к DLL-файлу. Следующий аргумент содержит GUID нужного интерфейса объекта. Он должен соответство- вать типу объявленного выше указателя на интерфейс. И в последнем аргументе клиентская программа передает адрес указателя на интерфейс, в который CoCrea- telnstance поместит адрес таблицы указателей на функции. Работа с методами СОМ-объекта case IDC_BUTTON3: GetDoubleFromTextWindow(hDlg,IDC_EDIT1,SArgument) ; Calculator->Add(Argument) ; Calculator->get_Register(SRegister) ; SetDoublelntoTextWindow(hDlg,IDC_EDIT2,Register,2) ; break ; Когда пользователь нажимает кнопку Add, обработчик сообщения WM_COMMAND переходит на эту ветвь case. Сначала из поля ввода IDC_EDIT1 извлекается значение аргумента. Затем через указатель на интер- фейс вызывается метод Add СОМ-объекта. После того как метод Add отработает, обработчик извлекает текущее значение скрытого регистра, обращаясь к методу доступа get_Register. Полученное значе- ние отображается в поле Register. Функции GetDoubleFromTextWindow и SetDoublelntoTextWindow принадле- жат компоненту GUIUtils. Для данного приложения файл, содержащий этот ком- понент, имеет расширение .срр, что позволяет без лишних осложнений оттранс- лировать его как программу на языке C++. Уничтожение объекта Calculator->Release() ; #if (WindowsCE CoUninitialize() ; #endif Сразу после создания функцией CoCreatelnstance COM-объект увеличивает на единицу свой счетчик ссылок. Чтобы объект мог уничтожить себя, клиентская программа вызывает его метод Release. Это приводит к уменьшению счетчика на единицу. Если в результате счетчик становится равным нулю, объект понимает, что активных клиентов у него не осталось и «заканчивает жизнь самоубийством».
ESHMUIIIII Использование СОМ-объектов Освободив объект, программа вызывает функцию CoUninitialize. Она сооб- щает SCM, что больше поддержка СОМ не понадобится. Как и в случае инициа- лизации, вызывать эту функцию необходимо лишь при работе на настольном ПК. Регистрация COM-сервера на Pocket PC Для регистрации COM-сервера на Pocket PC нужно предпринять несколько шагов. На КПК должна быть установлена специальная программа для регистра- ции и удаления СОМ-объектов. ПРИМЕЧАНИЕ С помощью программы Asynch Manager загрузите программу regsvrce. ехе в кор- невую папку Pocket PC. Обычно этот файл находится в папке C:\Windows СЕ Tools\wce300\MS Pocket PC\target\mips. Последний компонент пути соответству- ет целевой платформе. Для выполнения загруженной программы есть всего два способа. Первый - воспользоваться INF-файлом и программой установки. При таком подходе ника- кого взаимодействия с пользователем не происходит. Но для тестирования был бы более предпочтителен интерактивный путь. К сожалению, у программы regsrvce.exe нет графического интерфейса. Поэтому на сайте этой книги (http://www.osborne.com) имеется программа Registration- MgrProgram, реализующая дружелюбный интерфейс к regsvrce.exe (рис. 12.20). Regisltation Mgi Ptogram Quit М Biowse |cOMSwveiPiogram.dll | UnRegister~| < 1. Воспользоваться диалоговым окном File Open для выбора DLL 2. Появляется путь к выбранной DLL 3 . Нажать кнопку Register или Unregister Рис. 12.20. Регистрация СОМ-объекта на Pocket PC При нажатии кнопки Browse открывается окно File Open для выбора файла. С его помощью пользователь может найти нужный DLL-файл. Имя выбранного
337 Резюме файла появляется в поле посередине клиентской области. При желании можно просто ввести путь к серверу в этом поле. После этого нажатием кнопки Register или Unregister пользователь соответственно регистрирует или исключает СОМ- объект. ПРИМЕЧАНИЕ Программа запускает отдельный процесс для исполнения regsvrce.exe, а затем ждет, когда он завершится. Резюме Эта глава посвящена использованию COM-объектов на Pocket PC под управ- лением Windows СЕ. Вот что следует запомнить: □ СОМ-объект проще всего создать с помощью мастеров ATL, входящих в состав Visual Studio; □ для создания СОМ-объекта нужно сначала подготовить для него окруже- ние, а затем вставить туда сам объект; □ из всех контекстов исполнения СОМ самым быстрым является внутри- процессный сервер; □ несколько клиентов могут совместно использовать один и тот же СОМ- объект, что экономит ограниченные ресурсы Pocket PC; □ COM-клиент пользуется указателем на интерфейс, который ведет на таб- лицу указателей на функции, реализующие методы СОМ-объекта; □ таблица указателей на функции заполняется в момент загрузки СОМ- объекта в память; □ для регистрации СОМ-объекта на Pocket PC без вмешательства человека нужны INF-файл и программа regsvrce.exe; □ для интерактивной регистрации СОМ-объекта нужна специальная про- грамма с графическим интерфейсом и regsvrce.exe. Примеры программ в Web На сайте http://www.osborne.com имеются следующие программы: Описание Папка COM-сервер для настольного ПК COM-сервер для Pocket PC Менеджер регистрации для настольного ПК Менеджер регистрации для Pocket PC COM-клиент для настольного ПК СОМ- клиент для Pocket PC COMServerProgram COMServerProgramPPC RegistrationMgrProgram RegistratiouMgrProgramPPC COMCIientProgram COMCIientProgramPPC
338 Illi Использование COM-объектов Инструкции по сборке и запуску СОМ-сервер для настольного ПК 1. Запустите Visual C++6.0. 2. Откройте проект COMServerProgramPPC.dsw в папке COMServerProg- гатРРС. 3. Соберите DLL. СОМ-сервер для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект COMServerProgramPPC.vcw в папке COMServerProg- гатРРС. 7. Соберите DLL. 8. Убедитесь, что DLL успешно загрузилась в КПК. Менеджер регистрации для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект RegistrationMgrProgram.dsw в папке RegistrationMgr- Program. 3. Соберите программу. 4. Запустите программу. 5. Нажмите кнопку Browse. 6. Должно открыться диалоговое окно File Open. 7. Найдите на диске файл COMServerProgram.dll. 8. Нажмите кнопку Register. 9. Выберите пункт меню Quit. 10. Окно закроется, так как приложение завершило работу. Менеджер регистрации для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Загрузите программу regsvrce.exe в корневую папку Pocket PC. Обычно этот файл находится в папке C:\Windows СЕ Tools\wce300\MS Pocket PC\tar- get\mips. Последний компонент пути соответствует целевой платформе. 6. Запустите Embedded Visual C++ 3.0. 7. Откройте проект RegistrationMgrProgramPPC.vcw в папке Registration- MgrProgramPPC.
339 Примеры программ в Web 8. Соберите программу. 9. Убедитесь, что программа успешно загрузилась в КПК. 10. На КПК запустите File Explorer. 11. Перейдите в папку MyDevice. 12. Запустите программу RegistrationMgrProgram. 13. Коснитесь стилосом кнопки Browse. 14. Должно открыться диалоговое окно File Open. 15. Найдите на диске файл COMServerProgram.dll. 16. Коснитесь стилосом кнопки Register. 17. Выберите пункт меню Quit. 18. Окно закроется, так как приложение завершило работу. СОМ-клиент для настольного ПК 1. Запустите Visual C++ 6.0. 2. Откройте проект COMCIientProgram.dsw в папке COMCIientProgram. 3. Соберите программу. 4. Запустите программу. 5. Введите число в поле Register. 6. Нажмите кнопку Set. 7. Введенное число будет записано в скрытый регистр. 8. Введите число в поле Argument. 9. Нажмите кнопку, соответствующую какой-нибудь операции. 10. Содержимое поля Register изменится в соответствии с выполненной опе- рацией. 11. Выберите пункт меню Quit. 12. Окно закроется, так как приложение завершило работу. 13. Выполните программу RegistrationMgrProgram, чтобы отменить регист- рацию СОМ-объекта. СОМ-клиент Для Pocket PC 1. Подключите подставку КПК к настольному компьютеру. 2. Поставьте КПК на подставку. 3. Попросите программу ActiveSync создать гостевое соединение. 4. Убедитесь, что соединение установлено. 5. Запустите Embedded Visual C++ 3.0. 6. Откройте проект COMClientProgramPPC.vcw в папке COMCIientProgram РРС. 7. Соберите программу. 8. Убедитесь, что программа успешно загрузилась в КПК. 9. На КПК запустите File Explorer. 10. Перейдите в папку MyDevice. 11. Запустите программу COMCIientProgram. 12. Введите число в поле Register. 13. Коснитесь стилосом кнопки Set.
340 111’ Использование COM-объектов 14. Введенное число будет записано в скрытый регистр. 15. Введите число в поле Argument. 16. Коснитесь стилосом кнопки, соответствующей какой-нибудь операции. 17. Содержимое поля Register изменится в соответствии с выполненной опе- рацией. 18. Выберите пункт меню Quit. 19. Окно закроется, так как приложение завершило работу. 20. Выполните программу RegistrationMgrProgram, чтобы отменить регист- рацию СОМ-объекта.
Предметный указатель « «BUTTON», класс встроенного элемента управления, 233 «EDIT», класс встроенного элемента управления, 235 А ActiveSync, и преобразование растовых изображений,184 Add Method to Interface Wizard, мастер, 324, 328 AddRef, метод (COM), 312 ANSI С, стандартная библиотека, 33 ApplyKernelToBitmap, компонент BitmapUtilities, 167 ASCII-коды символов, 138 ATL (ActiveX Template Library). См. также COM, 318 Add Method to Interface Wizard, мастер, 324 COM AppWizard, мастер, 319 New Project Information, диалоговое окно, 321 New Project, диалоговое окно, 320 Object Wizard, мастер, 320 вкладка Class View, 323,325 реализация методов объекта, 325 шаги создания СОМ-объекта, 319 В BACKSPACE клавиша, программа ввода символов, 127 BeginPaint, функция, 51 biClrlmportant поле, растровое изображение, 158 biClrUsed поле, растровое изображение, 158 BitmapButtonMgr, компонент, 257 абстрактный тип данных, 261 заголово1 Ьайл,260 обзор, 257 обработчик DlgOnDrawItem, 261 реализация, 262 функция DestroyBitmapButton, 261 BitmapFile Header, секция BMP-файла, 156 BitmapInfoHeader, секция BMP-файла, 157 BitmapUtilities, компонент, 158,163 InMemoiyDC, контекст устройства, 166 алгоритм обнаружения краев, 167 листинг, 163 ресурс, представляющий растровое изображение, 165 функция ApplyKernelToBitmap, 167 функция CreateDIBSection, 165 функция CreateFile, 165 функция DeleteDC, 166 функция DisplayABitmap, 158, 165, 167 функция ReadFile, 165 функция StretchBlt, 158,166 ВМР-файл, 156 С CALLBACK, уточнение функция DlgProc, 78 CaretMgr, компонент, 213 CComCoClass, класс, 327 CComModule, класс, 330 CComObjectRootEx, класс, 327 СЕ Executive, подсистема Windows СЕ, 19 CheckMenuItem, функция Win32 API, 226 Class View, вкладка ATL (ActiveX Template Library), 325 мастер Object Wizard, 323 CloseHandle, функция Win32 API, 303 CloseParameterDBase, функция, 282
342 Illi Предметный указатель CLSlD, идентификатор класса в СОМ, 309,312 CLSIDFromProgID, функция Win32 API, СОМ, 315 coclass, раздел IDL-файла, 331 CoCreatelnstance, функция Win32 API, СОМ, 315,335 СОМ (модель компонентных объектов), 308 AddRef, метод, 312,313 CLSIDFromProgID, функция, 315 CoCreatelnstance, функция, 315 Createlnstance, метод, 316 DEFINE_GUID, макрос, 313 DLLGetClassObject, функция, 315 GUID (глобально уникальный идентификатор), 309, 311,313 HKEY_CLASSES_ROOT, улей реестра, 309 IID (идентификатор интерфейса), 312 InprocServer32, ключ реестра, 315 IUnknown, интерфейс, 312 LoadLibrary, функция, 315 Queryinterface, метод, 312,313 Release, метод, 312,313,316 SCM (диспетчер сервисов), 309, 311,315 архитектура, 308 библиотека ATL, 318 внутрипроцессный сервер, 317 динамическое создание объектов, 310 Диспетчеризация,318 доступ к объекту из программы, 310 заместители и заглушки, 318 идентификатор класса (CLSID), 309,312,315 инструмент для генерирования GUID, 313 интерфейсы, 311,312 карта СОМ, 328 клиенты,332 контексты выполнения, 316 локальный сервер, 317 маршалинг интерфейсов, 317, 318 последовательность создания объекта, 316 представление простого СОМ- объекта в виде интерфейса, 311 регистрация СОМ-сервера на Pocket PC, 336 регистрация объектов, 314 реестр, 309,313 счетчик ссылок, 312 удаленный сервер, 317 язык C++, 308 COM AppWizard, ATL, 319 CommandBar_Create, функция Win32 API, 82 CommandBar_InsertMenubar, функция Win32 API, 82 COM-клиенты, 332 COM-объекты, 326 CComCoClass, класс, 327 CComModule, класс, 330 CComObjectRootEx, класс, 327 DllGetClassObject, функция, 330 ICalculatorMgr, интерфейс, 327 InprocServer32, ключ, 332 OBJECTENTRY, макрос, 330 STDMETHOD, макрос, 328 STDMETHODIMP, макрос, 328 глобальные функции и объекты, 329 карта СОМ, 328 мастер Add Method to Interface Wizard, 328 объявление класса, 326 получение информации об интерфейсе, 333 программирование интерфейсов, 334 сценарий реестра, 331 файл описания интерфейса, 330 шаблонные классы, 327 CoUninitialize, функция Win32 API, СОМ, 336 CreateBitmapButton, функция, компонент BitmapButtonMgr, 261, 262 CreateCompatibleDC, функция Win32 API, 166
Предметный указатель Н111П! 343 CreateDIBSection, функция Win32 API, 165 CreateEvent, функция Win32 API, 303 CreateFile, функция Win32 API, 165 Createlnstance, метод, COM, 316 CreatePen, функция Win32 API, 112 CreateTabPage, функция, компонент TabPageMgr, 268, 270 CreateThread, функция Win32 API, 301 CreateWindowEx, функция Win32 API, 49 D DataMgr, компонент, 84, 200, 212 инкапсуляция,85 проект минимальной диалоговой программы, 67, 87 реализация синхронизации, 300 DBaseMgr, компонент, программа сохранения параметров, 277 DBRecordMgr, компонент, программа сохранения параметров, 277 DefaultMgr, компонент, программа работы с меню, 196,197,211 DefaultValues, структура данных, программа рабоы с меню, 196 DEFINEGUID, макрос, СОМ, 313 DeleteDC, функция Win32 API, 166 DeleteObject, функция Win32 API, 175 DeletePen, функция Win32 API, ИЗ DestroyBitmapButton, функция, компонент BitmapButtonMgr, 261 DestroyCaret, функция Win32 API, 139 DialogBox, функция в программе ввода символов, 132 и функция WinMain, 76 DispatchMessage, функция Win32 API, 29 DisplayABitmap, компонент BitmapUtilities, 158, 165, 166 в программе обработки изображений,181 DisplayAMenu, компонент PortabilityUtils, 81 DisplayBitmapButton, компонент BitmapButtonM-r, 263 DrawLineShapeAt, функция анализ эффективности инкапсуляции, 114 обработчики сообщений в простой программе рисования, 111 DrawObjMgr, компонент, 200, 208 DrawObjectRecordType, структура, 209 DrawObjMgr.h, заголовочный файл, 208, 209 DrawShape, функция, 209, 210 PutDatalntoShape, функция, 209 DrawOps, компонент, 111 DrawRectangleShapeAt, функция, 111 готовые кисти, 113 объявление переменных, 112 описатели инструментов, 112 DrawRectangleShapeAt, функция, компонент DrawOps, 111 DrawShape, функция, компонент DrawObjMgr, 209 применение в UserlnputMgr, 218 Е EndDialog, функция Win32 API, 111 EndPaint, функция Win32 API, 51 ExtTextOut, функция Win32 API, 144 F File Open, диалоговое окно, 151 FileNameMgr, компонент, 160 GetlnputFileName, функция, 160 GetOpenFileName, функция Win32 API, 160 GetSaveFileName, функция Win32 API, 160 OFN_LONGNAMES, символическая константа, 162 OPENFILENAME, структура, 162 ReformatFilterString, функция, 162 фильтр, 161 FixWindowPosition, функция, компонент PortabilityUtils, 83 G GDI (интерфейс графических устройств), 17, 22, 24
344 Illi Предметный указатель бинарные растровые операции, 126 команды рисования, 25 контекст устройства, 25,26, 93 куча, ИЗ операции рисования, 95 отображение и рисование графики, 24 пространство рисования, 25 GetBitmapDimensions, функция, компонент BitmapUtilities, 170 GetClientRect, функция Win32 API, 171 GetDoubleFromText Window, функция, компонент GUIUtils, 335 GetKeyState, функция Win32 API, 140 GetMessage, функция Win32 API, 46 GetStockBrush, функция Win32 API, 113 GetTabPage, функция, компонент TabPageMgr, 269 GetTextHeight, функция, компонент TextFns, 141 GetTextRectangle, функция, компонент TextFns, 132 GetText Width, функция, компонент TextFns, 134 GUI (графический интерфейс пользователя), 17 COM-клиент, 333 встроенные элементы управления, 231 клиентская область, 18 мастер Message Cracker Wizard, 70 минимальная диалоговая программа, 64 полоса заголовка, 17 полоса меню, 17 программа анимации изображений, 177 программа ввода/вывода символов, 128 программа вывода заставки, 171 программа дружелюбной полосы прокрутки, 242 программа обработки изображений, 151 программа работы с меню, 193 программа рисования эластичного контура, 118 программа сохранения параметров, 274 простая программа анимации, 91 GUID (глобально уникальный идентификатор), СОМ, 309, 311, 313 IDL-файл, 331 инструмент генерирования, 313 GUIUtils, компонент, 244, 245 GWES (Graphics, Windowing and Event Subsystem), 19,21,22 блок-схема, 23 и событийно-ориентированное программирование, 24 компонент USER, 22 прерывания, 24 системная очередь, 22 Н HANDLE_DLG_MSG, макрос анализ проекта минимальной диалоговой программы, 87 анализаторы сообщений, 68 сообщение WM_POSITIONCARET, 136 HideCaret, функция Win32 API, 139 HKEY_CLASSES_ROOT, улей реестра, 309,313,332 I ICalculatorMgr, интерфейс, 327 IDL (язык описания интерфейсов), 330 GUID’bi, 331 раздел coclass, 331 раздел library, 331 IID (идентификатор интерфейса), СОМ, 312 INFINITE, константа, синхронизация потоков, 304 INI-файлы, программа сохранения параметров, 278 InvalidateRect, функция Win32 API в компоненте UserlnputMgr, 218 в программе рисования эластичного контура, 125 в простой програмг1 нимации, 109
Предметный указатель IIIIIM! 345 обновление клиентской области окна, 30 реализация обработчика события WM_CHAR, 142 lUknown, интерфейс, СОМ, 312 К KernelMgr, компонент, 162 KillTimer, функция Win32 API, 111 L LoadLibrary, функция Win32 API, 315 LoadMenu, функция Win32 API, 83 LPRECT, тип данных, 133 м MaintainWindowPosition, функция, компонент Portability Utils, 83 MAKEINTRESOURCE, макрос Win32 API, 77 MapVirtualKey, функция Win32 API, 140 MENU_OFFSET, константа, 170 MENUITEM, описание пункта меню, 75 Message Cracker Wizard, мастер, 67, 70 вкладка Data Review, 72 вкладки, 70 генерируемый код, 72 кнопки, 71 способы копирования, 71 О Object Wizard, мастер, 320 OBJECT_ENTRY, макрос, СОМ, 330 OFN_LONGNAMES, константа, 162 OnSelChanged, функция, компонент TabPageMgr, 268 OPENFILENAME, структура, 162 Р ParameterDBMgr, компонент, 276, 277, 281 POINT, тип данных, 119 Portability Utils, компонент, 81 Display AMenu, функция, 81 FixWindowPosition, функция, 83 IDCB_MAIN, идентификатор, 82 LoadMenu, функция, 83 MaintainWindowPosition, функция, 83 SetWindowPos, функция, 83 флаг WindowsCE, 82 PostMessage, функция Win32 API, 51,143 PostQuitMessage, функция Win32 API, 51 ProcessIntegerEditNotification, функция, компонент GUIUtils, 248 ProcessScrollMessage, функция, компонент GUIUtils, 247 PutDatalntoShape, функция, компонент DrawObjMgr, 209 PutTabPage, функция, компонент TabPageMgr, 269 Q Queryinterface, метод, COM, 312 R R2_COPYPEN, растровая операция, 126 R2_NOTXORPEN, растровая операция, 126 Raw Input Thread (RIT), 22 RawData, структура, 295, 300 RGB, макрос, 103 RGBQuadTable, конвейер обработки изображения, 157 s SCM (диспетчер сервисов), 309,311,315 SelectObject, функция Win32 API, 112 SendMessage, функция Win32 API, 143 SetBkColor, фунция Win32 API, 144 SetCaretPos, функция Win32 API, 141 SetTimer, функция Win32 API, 108 STDMETHOD, макрос, COM, 328 STDMETHODIMP, макрос, COM, 328 StretchBlt, функция Win32 API компонент BitmapUtilities, 158,166 программа анимации изображений, 177,178 SynchMgr, компонент, 300, 303 T Tab Control, элемент управления, 265
346 Illi Предметный указатель TabCtrl_GetCurSel, макрос, 268 TabPageMgr, компонент, 264 AddTab, функция, 268 диалоги без рамки, 265 компоновка с библиотекой стандартных элементов, 269 макрос TabCtrl_GetCurSel, 268 обработчик сообщения WMJNITDIALOG, 268 обработчик сообщения WM_NOTIFY, 267 размещение вкладок в родительском диалоге, 265 реализация, 270 родительский диалог, 263,265,266 создание диалоговых процедур, 264 функция ChildYDlgProc, 266 функция CreateTabPage, 268 функция GetTabPage, 269 функция OnlnitParentXDialog, 267 функция OnSelChanged, 268 функция ParentXDlgProc, 266 функция ParentXOnSelChanged, 268 функция PutTabPage, 269 функция TabXDlgProc, 268 TCHAR, тип данных, 34 TranslateMessage, функция Win32 API, 46,140 и Unicode, кодировка, 34 USER, компонент Windows СЕ, 22 обработка сообщений, 29 W WaitForMultipleObjects, функция Win32 API, 305 WaitForSingleObject, функция Win32 API, 305 Windows СЕ, операционная система, 16 архитектура, 18 графический интерфейс пользователя, 17 исполняющая подсистема (СЕ Executive), 19 логическая структура программы, 28 менеджер ввода/вывода, 20 менеджер памяти, 20 менеджер процессов, 20 перенос программ, 53 подсистема интерфейса графических устройств (GDI), 22 подсистема управления графикой, окнами и событиями (GWES), 19,22 прикладной уровень, 19 ядро, 20 WindowsCE, флаг, 82,86,240 windowsy.h, заголовочный файл, 69 WinMain, функция, 41 в программе работы со встроенными элементами управления, 240 заголовочные файлы, 42 и подсистема GDI, 44 листинг, 41 лоническая структура программы, 28 модификация при переносе, 53 объявление переменных, 43 оконный класс, 45 описатель экземпляра, 44 сигнатура, 42 структура WNDCLASSEX, 43 уточнение CALLBACK, 42 функция GetMessage, 46 функция Show Window, 46 функция TranslateMessage, 46 WinProc, функция, 47 координаты клиентской области, 50 листинг, 47 объявление переменных, 48 стили, 49 функция BeginPaint, 51 функция CreateWindowEx, 49 функция EndPaint, 51 функция PostQuitMessage, 51 функция SetWindow Pos, 49 А Акселераторы, и функция WinMain, 76 Алгоритм понижения приоритета, 293 Анализ проекта простой программ, 58 Анализаторы сообщений, 67
Предметный указатель IIIIIUM 347 макрос HANDLE_DLG_MSG, 68 обработчик DlgOnCommand, 69 обработчик сообщения WMPOSITIONCARET, 136 параметр wParam, 68,69 файл windowsx.h, 68 файл windowsy.h, 68 Анимация. См. программа анимации изображений; простая программа анимации Аппаратная независимость, при выводе изображений, 98 Атомарные действия, проблема синхронизации,297 Б Библиотека стандартных элементов управления, компоновка с программой, 269 Бинарная растровая операция, 126 В Векторные операции рисования, 95 Видеоконвейер, 98 Виртуальная клавиша, код, 138,139 Виртуальное пространство рисования, 25 Внутрипроцессный сервер, СОМ, 317 Возвратные переходы состояний, 202, 227 Встроенные элементы управления, 230 включение в пользовательский интерфейс, 237 класс «BUTTON», 233 класс «EDIT», 235 класс «LISTBOX», 233 классы, 233 кнопка, 231 комбинированный список, 231 многострочное поле ввода, 231 однострочное поле ввода, 231 отношение родитель/потомок, 234 переключатель, 231 последовательность событий, 235 редактор ресурсов, 234 скрытая оконная процедура, 234 сообщения и < войства, 235 список, 231 статический текст, 231 стили,233 флажок, 231 Вывод изображения, 98 аппаратная независимость, 98 видеодрайверы, 99 видеоконвейер, 98 модель рисования в Windows, 99 обработчик сообщения WM_PAINT, 99 обработчик сообщения от таймера, 99 отсечение, 98 принудительная перерисовка клиентской области, 99 Г Главное меню, 194 Глобальные переменные, анализ проекта, 60 Готовые кисти,114 д Динамическое связывание, СОМ, 308 Диспетчеризация, СОМ, 318 Длинные указатели, 134 Дочерние потоки, 302 3 Заголовок окна, 256 И Идентификатор диалога, 74 Иконка идентификатор ресурса, 54 Индикатор связи, 65 Инкапсуляция, 102 СОМ, 312 анализ эффективности, 114 в программе работы с сменю, 200 в простой программе анимации, 114 и отладка, 102 и повторное использование, 102 и расширяемость, 103 компонент DataMgr, 85 компонент DrawOps, 104 обработка растровых изображений, 156
348 Предметный указатель растровых изображений, 149 реализация синхрониации потоков, 300 сравнение с копированием текста, 102 функции работы с текстом, 133 К Каре, 128, 141 Квант времени, 21 Кисти, стили, 94 Клиентская область графический интерфейс пользователя, 18 координаты и функция WinProc, 50 обновление, 30 принудительная перерисовка, 99 Кнопка, встроенный элемент управления, 231 Кодовая точка, Unicode, 34 Команды рисования, GDI, 25 Комбинированный список, встроенный элемент управления, 231 Компонент UserlnpuMgr, 205 заголовочный файл, 214 псевдокод, 216 реализация,215 спецификация диаграммы состояний, 218 функция DisplayDrawObject, 218 функция ProcessUserlnput, 219 функция PutDatalntoShape, 217 Конвейер обработки изображения, 155 BitmapUtilities, компонент, 158 Display, функция, 156 секция BitmapBits, 157 секция BitmapFileHeader, 156 секция BitmapInfoHeader, 157 секция RGBQuadTable, 157 фаза записи, 155 фаза отображения, 155 фаза сохранения, 155 фаза чтения, 155 Конечный автомат, для программы работы с меню, 201 Контекст устройства, 25, 26, 92 как ресурс GDI, 93 Контекстное переключение, 289 Контексты выполнения СОМ-сервера, 316 Копирование текста, сравнение с инкапсуляцией, 102 Куча, GDI, ИЗ, 175 Л Логическая структура программы, 28 обновление клиентской области, 30 обработка сообщений, 29 функция WinMain, 28 функция WndProc, 28 м Маршалинг интерфейсов, 317 Менеджер ввода / вывода, Windows СЕ, 20,21 Менеджер памяти, Windows СЕ, 20 Менеджер процессов, Windows СЕ, 20 Мигание, устранение, 91 Минимальная диалоговая программа, 63 анализ проекта, 87 обработчики сообщений, 79 отладка, 86 перенос на КПК, 85 полоса меню, 64 пользовательский интерфейс, 64 принцип единственного способа выполнения операции, 64 проектирование, 64 реализация, 73 флаг WindowsCE, 86 Многозадачность вытесняющая, 290 Многопоточные приложения, 288 одновременность выполнения, 288 синхронизация, 295 Многострочное поле ввода, встроенный элемент управления, 231 Многострочное статический текст, встроенный элемент управления, 231 Н Набор инструментов рисования, 92 Ниспадающее подмен!' 94,220
Предметный указатель НИШ 349 Обнаружения краев, 150 алгоритм,167 Объекты рисования, программа работы с меню, 197,199 Однострочное поле ввода, встроенный элемент управления, 231 Описатель экземпляра, 39,44 Отладка и инкапсуляция,102 минимальная диалоговая программа, 86 Отсечение, 96 вывод изображения, 98 и система координат, 97 прокрутка окна, 95 Охватывающий прямоугольник в простой программе анимации, 109 при реализации компонента для работы с текстом, 134 при реализации обработчика сообщения WM_CHAR, 142 программа ввода символов, 129 П Переключатель, встроенный элемент управления, 231 Планирование потоков, 291 Полоса заголовка, 17 Полоса меню графический интерфейс пользователя, 17 компонент PortabilityUtils, 82 минимальная диалоговая программа, 64 Помеченные пункты меню, 195 Потоки. См. Многопоточные приложения Преобразование программы для Windows в программу для Windows СЕ, 53 модификация WinMain, 53 модификация WinProc, 56 Преобразование растровых изображений, отмена в ActiveSvnc, 186 Прерывания, 24 Прикладной уровень, логическое проектирование программы, 19 Приоритеты, управление, 292 Программа «прыгающие квадратики»,294 влияние приоритетов потоков на производительность, 295 Программа анимации изображений изображение переднего плана, 177 обзор, 176 пользовательский интерфейс, 177 реализация, 105,178 фоновое изображение, 177 функция StretchBlt, 178 Программа ввода символов, 127 добавление переменных для хранения состояния и текстовой строки, 135 инкапсуляция функций для работы с текстом, 133 клавиша BACKSPACE, 127 нестандартные сообщения, 131 обработчики сообщений, 137 охватывающий прямоугольник, 129 переменная TextData, 130 переменная TextLocation, 129 переменные, 135 положение каре, 129 пользовательский интерфейс, 127 последовательность действий пользователя, 127 режим ввода текста, 127 функция DialogBox, 132 функция GetTextRectangle, 132 Программа вывода заставки, 171 модель рисования, 173 пользовательский интерфейс, 171 реализация, 173 Программа дружелюбной полосы прокрутки, 241 иерархические интерфейсы, 242 парное поле ввода, 241, 242 пользовательский интерфейс, 242 последовательность действий, 242 реализация,173,244
350 Illi Предметный указатель Программа обработки изображений, 149 анализ организации, 155 загрузка и показ файла с растровым изображением, 151 конвейер, 155 обнаружение краев, 150,154 пользовательский интерфейс, 150 реализация, 160 Программа работы с меню, 192 CheckMenuItem, функция, 226 DefaultValues, структура, 196 анализ кода, 208 взаимодействия, 205 возвратные переходы состояний, 202 компонент CaretMgr, 213 компонент DataMgr, 196,200,212 компонент DefaultMgr, 197,211 компонент DrawObjMgr, 200,208 компонент UserlnputMgr, 205,214 ниспадающие подменю, 194,221, переходы состояний, 201 пользовательский интерфейс, 193 помеченные пункты меню, 195 расширяемость, 201 реализация,207 состояния рисования, 201 таблица действий, 204 уровень пользовательского интерфейса, 205 уровень принятия решений, 205 уровень управления данными, 205 Программа рисования эластичного контура, 118 SetCapture, функция, 124 UpdateWindow, функция, 124 бинарные растровые операции, 126 обработки сообщений о перемещении мыши, 120 пользовательский интерфейс, 118 реализация рисования, 126 статические переменные, 123 стирание, 126 функция InvalidateRect, 125 Программа сохранения параметров, 274 INI-файлы, 278 анализ кода, 283 записи по умолчанию, 281 компонент ParameterDBMgr, 276, 281, 283 метод CloseParameterDBase, 282 метод GetDoubleValueFrom- ParameterDBase, 282 метод GetRecordFromDBase, 277 метод GetStringValueFrom- ParameterDBase, 282 метод GetValuesFrom- ParameterDBase, 282 метод OpenParameterDBase, 282 многоуровневый дизайн, 275,283 операции, 282 организация записей, 280 пользовательский интерфейс, 274 форматы хранения, 278 функция ReadRecordFromDBase, 277, 283 функция SetDefaultValues, 281 функция SetDoublelnto- Text Window, 282 шаги настройки, 279 Прокрутка окна, 95 Простая программа анимации, 90 анализ эффективности инкапсуляции, 114 обработчики сообщений, 107 пользовательский интерфейс, 91 реализация,105 рисование, 91 устранение мигания, 91 Пространство рисования, GDI, 25 Р Разделяемые данные, синхронизация, 295 Растровая операция, 126 Растровые изображения инкапсуляция,149 обзор, 149 программа ActiveSync, 184 программа анимации, 176 программа вывода заставки, 171 программа обработки изображений,150
Предметный указатель IIIIIHHHI 351 ресурсы, 150 Расширяемость анализ проекта, 58 и инкапсуляция, 103 программы работы с меню, 201 Редактор диалогов, дружелюбная полоса прокрутки, 245 Реестр и СОМ, 309,313 ключ InprocServer32,315 программа сохранения параметров, 278 Ресурсный сценарий, 225 Рисование, 91 векторные операции, 95 вывод изображения, 98 набор инструментов, 91 отсечение, 95 стили кистей, 94 стили перьев, 94 С Сигнатура функции WinMain, 43 Символьные строки, программирование, 34 Синхронизация, 295 атомарные действия, 297 и поток WinMain, 299 разделяемые данные, 295 реализация, 299 создание объектов, 303 структура RawData, 296 точки вытеснения, 297 Системная очередь, 22 Скрытая оконная процедура, встроенные элементы управления, 234 Событийно-ориентированное программирование, 24 События с автоматическим сбросом, 303 с ручным сбросом,303 События, механизм синхронизации, 303 Создание файла в Windows СЕ, 21 Список, встроенный элемент управления чч-| Статические переменные в программе рисования эластичного контура, 123 в простой программе анимации, 107 Стили элементов управления, 233 Строки ASCII-символов, функции манипулирования, 36 Структур данных, 37 Сценарий реестра, 331 Счетчик ссылок, 312 Т Таблица действий, для программы работы с меню, 203 Таблица указателей на функции, СОМ, 334 Таймер, 100 Точки вытеснения, 297 У Указатель на интерфейс, 334 Управление памятью, 60 Уровень пользовательского интерфейса, программа работы с меню, 205 Уровень принятия решений, программа работы с меню, 205 Уровень управления данными, программа работы с меню, 205 Уровни в архитектуре Windows СЕ, 19 программы работы с меню, 200 программы сохранения параметров, 275 проект минимальной диалоговой программы, 67 Уточнение, 42 Ф Флажок, встроенный элемент управления, 231 Формат сообщения Windows, 38 Ш Шаблонные классы, 327 Я Ядро, 20