Графика для Windows средствами DirectDraw

       

Программа Cursor


Программа Cursor использует описанную выше методику и выводит на экран изображение вращающейся спирали, меню задержки и курсор мыши. По умолчанию программа выводит кадры максимально часто, но меню задержки позволяет уменьшить частоту вывода за счет задержки в основном потоке (максимальная задержка равна 500 миллисекундам, при этом приложение замедляется до 2 FPS). Если бы курсор не управлялся отдельным потоком, его обновление происходило бы лишь с выводом очередного кадра. Но поскольку курсор мыши не зависит от основного потока, он нормально реагирует на действия пользователя при любой частоте вывода. Программа Cursor изображена на рис. 7.1.


Рис. 7.1. Программа Cursor

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

СОВЕТ

Как создать собственный курсор

Программа Cursor может работать с курсором любого размера. В версии программы на CD-ROM использован небольшой курсор (12x20 пикселей), но вы можете легко изменить этот стандартный размер. Для этого достаточно заменить cursor_08.bmp и/или cursor_24.bmp файлами с более крупными изображениями курсоров.

По умолчанию приложение работает в 8-битном видеорежиме и соответственно с 8-битным курсором. Многое зависит от вашего графического редактора, но, скорее всего, вы избавитесь от проблем с палитрой, если воспользуетесь файлом cursor_08.bmp

с CD-ROM как шаблоном для создания нестандартного курсора. С курсором формата True Color дело обстоит проще, но, чтобы воспользоваться им, придется слегка подправить функцию SelectInitialDisplayMode(), чтобы активизировать беспалитровый видеорежим вместо палитрового.



Программа Qwerty


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

Примеров хватает в избытке. Скажем, когда кто-нибудь чихнет, мы привыкли говорить: «Будь здоров». Когда-то считалось, что чихание — акт (или во всяком случае попытка) изгнания злого духа из тела. Сегодня даже самые религиозные люди вряд ли считают чихание чем-то вроде экзорцизма в миниатюре, и все равно, если вы чихнете, кто-нибудь непременно пожелает вам здоровья. Мы просто не задумываемся над тем, что означают эти слова.

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

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



Итак, в раскладке Qwerty была изначально заложена неэффективность. У компьютерной клавиатуры нет ни рычагов, ни бумаги с красящей лентой, однако в ней используется та же самая раскладка Qwerty. Давно появились новые, более эффективные раскладки (например, раскладка Дворака), но они до сих пор не прижились. Да и кому захочется снова учиться печатать?

Так или иначе, название программы Qwerty происходит от раскладки Qwerty — отчасти потому, что я не смог придумать другого имени, отчасти из уважения к исторической традиции. Программа Qwerty изображена на рис. 6.1.





Рис. 6.1. Программа Qwerty

На экране изображены буквы Qwerty и названия еще нескольких клавиш. Программа с помощью DirectInput обнаруживает нажатые клавиши и рисует их в наклонном начертании. Все остальные клавиши рисуются прямо.




Программа Smear


Перейдем к поддержке мыши в DirectInput. Она будет рассматриваться на примере программы Smear, которая отображает поверхность в центре экрана и затем сдвигает изображение в соответствии с перемещением мыши (см. рис. 6.2).



Программа SuperSwitch


Программа SuperSwitch, как и программа Switch, позволяет установить любой видеорежим, но, кроме того, для выбранного видеорежима можно задать и частоту смены кадров. Основной экран программы SuperSwitch выглядит так же, как и в Switch, но при выборе видеорежима появляется подменю с возможными частотами смены кадров (см. рис. 4.2).

Перед тем как инициализировать DirectDraw, программа выводит диалоговое окно с кратким описанием. В этом окне можно отключить смену частоты, и тогда программа работает точно так же, как и программа Switch.



Программа Switch


Программа Switch — полноэкранное приложение DirectDraw, которое выводит меню всех поддерживаемых видеорежимов и позволяет активизировать любой из них. На рис. 4.1 показано, как она выглядит.


Рис. 4.1. Программа Switch

Эта программа представляет собой модифицированное полноэкранное приложение, сгенерированное DirectDraw AppWizard. В программу было добавлено меню, позволяющее выбрать видеорежим (с помощью клавиш со стрелками) и активизировать его (клавишей Enter). Клавиша Escape завершает работу программы. Программа Switch также вычисляет и отображает FPS приложения (количество кадров в секунду).

В программе Switch мы научимся:

переключать видеорежимы;

выводить текст на поверхностях DirectDraw;

рассчитывать FPS приложения;

работать с цветовыми ключами.



Программные требования


Для работы с книгой необходимо иметь Windows NT4.0 или Windows 95. Кроме того, потребуется Visual C++ 5.0 или более поздней версии.

Вам также понадобится собственно DirectX версии 3a или выше (желательно DirectX 5). Учтите, что DirectX распространяется в двух видах: в runtime-варианте и в SDK. Runtime-вариант часто устанавливается программами, использующими DirectX, он также встроен в операционную систему Windows NT 4.0. На CD-ROM этой книги содержатся runtime-компоненты DirectX 5. С другой стороны, пакет DirectX SDK необходим для компиляции программ, написанных для DirectX. Он отсутствует на CD-ROM этой книги, однако его можно бесплатно скачать по адресу: www.microsoft.com/msdownload.



Прокрутка больших поверхностей


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

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

установка библиотеки DirectX 5;

поддержка широких поверхностей видеоустройством;

наличие достаточного объема видеопамяти.

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



в этой книге напрямую, ничто


Хотя Visual C++ 4.0 не используется в этой книге напрямую, ничто не помешает вам работать с ним. Но так как на CD-ROM находятся файлы проектов только для Visual C++ 5.0 (для которого обратная совместимость не предусмотрена), вам придется создать файлы рабочей области самостоятельно. Это делается так:

Создайте пустое приложение Win32.

Скопируйте CPP-, H- и RC-файлы из каталога с компилируемой программой в каталог с новой, пустой рабочей областью.

Включите файлы в пустой проект (командой Project|Add to project|Files).


Заодно можно настроить прекомпилированные заголовки (см. выше). Проект готов к компиляции.

Хотя пользователи Visual C++ 4.x смогут пользоваться программами из этой книги, DirectDraw AppWizard работать не будет. Если читатели проявят достаточный интерес, я могу создать соответствующую версию и поместить ее на Web-узел.


Решение проблемы курсора


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



Runtime-файлы DirectX


DirectX делится на две части: runtime-часть и SDK. Для разработки приложений необходимы обе половины, а для запуска программ DirectX достаточно лишь runtime-файлов.

Runtime-часть DirectX встроена в Windows NT версии 4.0 и выше. В Windows 95 этих файлов нет, но их можно легко найти и установить. К сожалению, в Windows NT версий ниже 4.0 программы, использующие DirectX, работать не будут.

Runtime-часть DirectX поставляется вместе со многими приложениями DirectX. Она также имеется на CD-ROM, прилагаемом к этой книге. Распространяемые runtime-файлы DirectX предназначены только для Windows 95, не пытайтесь устанавливать их для Windows NT.

Существует несколько способов узнать, установлена ли runtime-часть DirectX в вашей Windows 95. Во-первых, можно попытаться запустить любую демонстрационную программу, прилагаемую к этой книге или к DirectX SDK. Если программы работают, значит, runtime-часть установлена. Во-вторых, можно открыть панель управления и выбрать значок Add/Remove Programs. Если в открывшемся списке присутствует строка DirectX Drivers, значит, runtime-часть установлена. (Последний способ относится только к Windows 95. Хотя в Windows NT имеется встроенная поддержка DirectX, строка DirectX Drivers в диалоговом окне Add/Remove Programs не выводится).

СОВЕТ

Новая возможность DirectX 5

Начиная с DirectX 5, значок DirectX помещается на панель управления. Это мини-приложение обеспечивает более совершенные возможности для просмотра и изменения настроек DirectX, чем в предыдущих версиях.

Необходимо также проверить версию имеющейся у вас runtime-части. Всего существуют четыре версии библиотеки: 1, 2, 3 и 5 (версии 4 не было). В каждой версии используется своя runtime-часть. Поскольку работа DirectX основана на спецификации COM с ее мощной поддержкой совместимости с предыдущими версиями, новые версии runtime-частей DirectX должны нормально работать со старыми приложениями DirectX. Чтобы узнать, какая версия библиотеки установлена на вашем компьютере, вызовите диалоговое окно Add/Remove Programs, выделите строку DirectX Drivers и нажмите кнопку Add/Remove — появляется список всех установленных компонентов. Компоненты с номерами версий, начинающимися с 4.02, принадлежат DirectX 1. Номера версий, начинающиеся с 4.03, относятся к DirectX 2, с 4.04 — DirectX 3, и 4.05 — DirectX 5. На прилагаемом к книге CD-ROM содержится runtime-часть от DirectX 5.



поверхности


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

Для примера возьмем 8-битную поверхность (поскольку один пиксель в таких поверхностях представляется одним байтом, что упрощает вычисления). Предположим, ваше видеоустройство требует, чтобы во внутреннем (для DirectDraw) представлении ширина поверхности была выровнена по границам параграфов (то есть была кратна 4 байтам). В этом случае поверхность с шириной в 10 пикселей будет иметь внутреннюю ширину в 12 байт. Если мы заблокируем эту поверхность и назначим значения пикселей, предполагая, что развертка одной горизонтальной строки занимает 10 байт, изображение получится перекошенным. Проблему можно легко решить, если вместо ширины при развертке поверхностей будет использоваться шаг. При этом независимо от внутренней ширины поверхностей, используемой DirectDraw, вычисление адресов пикселей будет давать правильный результат. Ситуация поясняется на рис. 5.2.


Рис. 5.2. Небольшая 8-битная поверхность с разными значениями шага и ширины

Для беспалитровых поверхностей шаг и ширина поверхности уже не связаны между собой, поскольку каждый пиксель занимает несколько байт. К примеру, возьмем поверхность High Color (16-битную). При ширине поверхности в 5 пикселей каждая строка будет занимать 10 байт. Если видеоустройство требует, чтобы фактическая ширина поверхности выравнивалась по границе параграфа, DirectDraw создает поверхность с внутренней шириной в 12 байт (см. рис. 5.3).

Обратите внимание: если бы ширина 16-битной поверхности на рис. 5.3 была равна 6 пикселям вместо 5, шаг поверхности остался бы прежним, потому что в каждой строке остается свободное место для одного дополнительного пикселя.


Рис. 5.3. Небольшая 16-битная поверхность с разными значениями шага и ширины



Давайте рассмотрим еще два примера, на этот раз с 24-битными поверхностями. При 12-байтовой модели, использованной выше, и 24-битной глубине пикселя в одной строке можно будет хранить 4 пикселя и избежать потерь памяти. Но что произойдет, если поверхность имеет ширину в 5 пикселей? Шаг увеличится до 16 байт, а в каждой строке будет напрасно пропадать один байт. Обе ситуации изображены на рис. 5.4.

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



Рис. 5.4. Две небольшие 24-битные поверхности

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



Схемы получения данных


В зависимости от потребностей вашего приложения DirectInput может использоваться для получения данных двух видов: непосредственных (immediate) и буферизованных (buffered). Непосредственные данные описывают состояние устройства ввода на момент запроса данных, а буферизованные данные используют концепцию очереди для описания изменений в состоянии устройства (например, нажатий клавиш или осевых смещений).

С помощью непосредственных данных можно, например, определить, нажата ли некоторая клавиша в данный момент. Для клавиатуры получение непосредственных данных напоминает применение функции Win32 GetAsyncKeyState(). Непосредственные данные лучше всего работают при частом опросе устройства ввода (как правило, не реже 30 раз в секунду). Редкий опрос может привести к потере данных; если за время нахождения клавиши в нажатом состоянии клавиатура не опрашивалась, то приложение не узнает о наличии ввода.

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

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



Символическая константа INITGUID (устаревшая)


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

была определена в одном и только одном кодовом модуле, до включения заголовочных файлов COM (для DirectX заголовочные файлы COM включаются косвенно, из заголовочных файлов DirectX). Следовательно, в некоторых программах можно встретить константу INITGUID. Этот метод вызывает немало хлопот, особенно при работе с прекомпилированными заголовками, потому что он не позволяет включить заголовочные файлы DirectX в состав прекомпилированного заголовка.

Вы не встретите INITGUID в программах на CD-ROM, потому что, начиная с DirectX 3, появилось более удачное решение — вместо того, чтобы определять INITGUID, достаточно подключить к проекту файл DXGUID.LIB. На случай, если вы не знали…



Синхронизация потоков


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

Потоки координируются с помощью событий

(events), которые передают информацию о состоянии одного или нескольких потоков. Событие может быть установленным (signaled) или сброшенным (unsignaled). Конкретный смысл событий может быть разным, но обычно они сигнализируют о блокировке потока.

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

Блокировка потоков чаще всего используется для защиты совместных ресурсов от одновременного доступа со стороны нескольких потоков. Мутекс (mutex, сокращение от mutually exclusive, то есть «взаимоисключающий») представляет собой объект, который может в любой момент времени принадлежать

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

Критические секции (critical section), как и мутексы, используются для предотвращения одновременного доступа к ресурсу со стороны нескольких потоков. Однако если мутекс может синхронизировать межпроцессные потоки, критическая секция ограничивается потоками одного процесса. Ограничение компенсируется скоростью — критическая секция работает быстрее, чем мутекс.

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



Советы и рекомендации


В этом разделе рассматривается несколько не связанных друг с другом, но полезных тем. Сначала мы поговорим об одной ошибке DirectDraw, а затем узнаем кое-что о файлах DirectX. Напоследок я скажу пару слов о видеокартах на базе чипов 3Dfx.



Создание поверхностей


Остается лишь создать поверхности, используемые в приложении. После вызова SetDisplayMode() функция ActivateDisplayMode() вызывает еще три функции: CreateFlippingSurfaces(), StorePixelFormatData() и CreateCustomSurfaces(). Функция CreateFlippingSurfaces() создает первичную поверхность с возможностью переключения страниц. Функция StorePixelFormatData() используется для чтения и записи сведений о формате пикселей в данном видеорежиме. Эта информация может пригодиться при работе с видеорежимами High и True Color. Функция CreateCustomSurfaces() отвечает за создание и инициализацию вспомогательных поверхностей, специфических для данного приложения. Начнем с функции CreateFlippingSurfaces():


BOOL DirectDrawWin::CreateFlippingSurfaces() { if (primsurf) primsurf->Release(), primsurf=0;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof( desc ); desc.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; desc.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; desc.dwBackBufferCount = 1; HRESULT r=ddraw2->CreateSurface( &desc, &primsurf, 0 ); if (r!=DD_OK) return FALSE; DDSCAPS surfcaps; surfcaps.dwCaps = DDSCAPS_BACKBUFFER; r=primsurf->GetAttachedSurface( &surfcaps, &backsurf ); if (r!=DD_OK) return FALSE;

return TRUE; }


Функция CreateFlippingSurfaces() вызывается при каждой инициализации нового видеорежима, поэтому ее работа начинается с освобождения ранее созданных поверхностей функцией Release(). Затем она объявляет и инициализирует экземпляр структуры DDSURFACEDESC. Эта структура описывает тип создаваемой поверхности. В соответствии с требованиями DirectDraw необходимо установить флаги для всех инициализируемых полей. В нашем случае флаги DDSD_CAPS и DDSD_BACKBUFFERCOUNT говорят о том, что мы задаем возможности поверхности (поле dwCaps) и количество вторичных буферов (поле dwBackCount). В поле dwCaps устанавливаются три флага:

DDSCAPS_PRIMARYSURFACE

DDSCAPS_FLIP

DDSCAPS_COMPLEX

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

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

Флаг DDSCAPS_COMPLEX используется всегда, когда происходит присоединение поверхностей. В нашем случае первичная поверхность должна быть присоединена к поверхности вторичного буфера. Затем мы присваиваем полю dwBackBufferCount значение 1, показывая, что к создаваемой первичной поверхности должен быть присоединен один вторичный буфер.

Новая поверхность создается вызовом функции CreateSurface() интерфейса DirectDraw. Первым аргументом является указатель на структуру desc, а вторым - указатель на переменную DirectDrawWin::primsurf. Эта переменная объявлена защищенной (protected), поэтому мы можем использовать ее для доступа к первичной поверхности в своих программах. Третий аргумент функции CreateSurface() должен быть равен 0.

Вызов CreateSurface() создает две поверхности: первичную поверхность и вторичный буфер. Позднее указатель на вторичный буфер понадобится нам для подготовки кадров. Чтобы получить этот указатель, следует вызвать функцию GetAttachedSurface() интерфейса DirectDrawSurface и передать ей структуру DDSCAPS с описанием типа интересующей нас присоединенной поверхности. Задавая флаг DDSCAPS_BACKBUFFER, мы вызываем функцию GetAttachedSurface(), которая инициализирует переменную backsurf. Она, как и переменная primsurf, объявлена защищенной, поэтому классы, производные от DirectDrawWin, могут легко обратиться к вторичному буферу.

После того как указатели primsurf и backsurf будут инициализированы, ActivateDisplayMode() вызывает функцию StorePixelFormatData(). Эта функция с помощью функции GetPixelFormat() интерфейса DirectDrawSurface получает информацию о формате хранения цветовых RGB-составляющих для отдельных пикселей.


Формат пикселей зависит от видеокарты, а иногда даже от видеорежима, так что эти сведения оказываются полезными при прямых манипуляциях с поверхностями. Функция StorePixelFormatdata() выглядит так:

BOOL DirectDrawWin::StorePixelFormatData() { DDPIXELFORMAT format; ZeroMemory( &format, sizeof(format) ); format.dwSize=sizeof(format); if (backsurf->GetPixelFormat( &format )!=DD_OK) { return FALSE; }

loREDbit = LowBitPos( format.dwRBitMask ); WORD hiREDbit = HighBitPos( format.dwRBitMask ); numREDbits=(WORD)(hiREDbit-loREDbit+1);

loGREENbit = LowBitPos( format.dwGBitMask ); WORD hiGREENbit = HighBitPos( format.dwGBitMask ); numGREENbits=(WORD)(hiGREENbit-loGREENbit+1); loBLUEbit = LowBitPos( format.dwBBitMask ); WORD hiBLUEbit = HighBitPos( format.dwBBitMask ); numBLUEbits=(WORD)(hiBLUEbit-loBLUEbit+1);

return TRUE; }

Структура DDPIXELFORMAT используется в функции GetPixelFormat() для получения масок, показывающих, какие биты в каждом пикселе заняты красной, зеленой и синей цветовыми составляющими. Маски точно описывают формат пикселя, но на практике работать с ними оказывается не очень удобно. Вместо того чтобы просто сохранить полученные маски, мы на основании каждой из них инициализируем два целых числа. Первое число равно позиции младшего бита цветовой составляющей, а второе — количеству бит, необходимых для ее представления. Для поверхностей True color (24- и 32-битных) цветовые составляющие всегда представляются 8 битами, но для поверхностей High color (16-битных) это число изменяется (обычно 5, но иногда 6 для зеленой составляющей).

Класс DirectDrawWin содержит шесть переменных для описания формата пикселей: loREDbit, numREDbits, loGREENbit, numGREENbits, loBLUEbit и numBLUEbits. Они используются некоторыми функциями DirectDrawWin, однако эти переменные объявлены как защищенные (protected), поэтому к ним можно обращаться и из классов, производных от DirectDrawWin. Эти переменные будут рассмотрены в главе 5.

На этом инициализация приложения подходит к концу.


Функция ActivateDisplayMode() вызывает еще одну функцию, CreateCustomSurfaces(), которая создает вспомогательные поверхности, но к этому моменту инициализация DirectDraw уже завершена. Функция CreateCustomSurfaces() будет рассмотрена в следующем разделе.

Но сначала давайте подведем итоги. Приложение состоит из двух объектов, BounceWin и BounceApp. Объект BounceApp отвечает за создание объекта BounceWin, а BounceWin в свою очередь инициализирует DirectDraw. Сначала он обнаруживает все имеющиеся драйверы DirectDraw, выбирает один из них и использует его для создания экземпляра интерфейса DirectDraw2. Затем он обнаруживает видеорежимы, поддерживаемые инициализированным драйвером, выбирает один из режимов и активизирует его. Далее создается первичная поверхность с возможностью переключения страниц (и вторичным буфером) и, наконец, анализируется формат пикселей для активизированного видеорежима.

Приложение почти готово к работе, но пока у него нет графических данных. Мы подходим к следующему этапу.


Создание приложений DirectDraw


После знакомства с DirectDraw API мы поговорим о том, как эта библиотека используется для создания готовых приложений. В этом разделе рассматриваются некоторые общие принципы построения приложений DirectDraw.



Создание приложения Bounce


В Visual C++ Developer Studio вызовите DirectDraw AppWizard. Для этого воспользуйтесь командой File | New, затем выберите Project Workspace — откроется диалоговое окно, в левой части которого перечислены все возможные AppWizard. На рис. 3.1 изображено это окно с выбранным DirectDraw AppWizard.


Рис. 3.1. Диалоговое окно New Project Workspace

На рисунке указано имя проекта Bounce. По умолчанию оно определяет имя каталога с проектом, а также используется для присвоения имен ряду элементов проекта, включая некоторые классы C++.

После того как вы выберете DirectDraw AppWizard, введете имя проекта и нажмете кнопку Create, на экране появится первое диалоговое окно AppWizard (см. рис. 3.2).


Рис. 3.2. Диалоговое окно с заставкой DirectDraw AppWizard

Это окно в основном декоративное, но по ряду элементов оно похоже на остальные окна AppWizard. Например, в каждом окне имеются четыре кнопки, изображенные на рис. 3.2. Кнопки Back и Next позволяют перейти к следующему или вернуться к предыдущему окну. При нажатии кнопки Finish появляется диалоговое окно подтверждения, в котором можно просмотреть принятые вами решения. Кнопка Cancel возвращает вас в среду Visual C++. Нажмите кнопку Next, чтобы перейти к следующему окну.

Во втором окне DirectDraw AppWizard предлагается выбрать тип создаваемого приложения: полноэкранное или оконное. По умолчанию установлен переключатель Full-screen (для полноэкранного приложения). Наше изучение начнется именно с полноэкранных приложений, поэтому оставьте тип, выбранный по умолчанию. Второе окно AppWizard изображено на рис. 3.3.


Рис. 3.3. Диалоговое окно Application Type

Снова нажмите кнопку Next, и перед вами появится третье окно AppWizard. В нем указываются исходные параметры приложения. Содержимое этого окна зависит от типа приложения, выбранного на предыдущем шаге. Поскольку ранее было выбрано полноэкранное приложение, в окне Initial Settings можно задать разрешение и глубину пикселей для начального видеорежима.
Диалоговое окно Initial Settings изображено на рис. 3.4.



Рис. 3.4. Диалоговое окно Initial Settings

По умолчанию выбирается режим 640x480x16. Оставьте значения, принятые по умолчанию, и нажмите кнопку Next.

В четвертом окне предлагается выбрать содержимое приложения — None или Bitmap. При установке переключателя None генерируемое приложение не делает абсолютно ничего, а при установке Bitmap оно выполняет анимацию растрового изображения. Диалоговое окно Contents изображено на рис. 3.5. По умолчанию устанавливается переключатель Bitmap; именно этот вариант будет использован в приложении Bounce.



Рис. 3.5. Диалоговое окно Contents

В пятом и шестом окнах диалога можно переопределить имена классов C++, по умолчанию выбираемые для приложения. Имена классов отображаются в двух текстовых полях; оставьте их без изменений. Диалоговое окно Class Names изображено на рис. 3.6.



Рис. 3.6. Диалоговое окно Class Names

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

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



Рис. 3.7. Диалоговое окно подтверждения


Специализированные классы


Классы DirectDraw проектировались как основа для классов, реализующих специфические возможности приложения. В случае приложения Bounce эти возможности обеспечиваются классами BounceWin и BounceApp.

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

Выбирать исходный видеорежим. При запуске приложения класс DirectDrawWin опознает все возможные видеорежимы. Производный класс должен выбрать видеорежим, который изначально устанавливается в приложении.

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

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

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

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



Спецификация COM фирмы Microsoft


Библиотека DirectDraw реализована в соответствии со спецификацией COM (многокомпонентная модель объекта, Component Object Model) фирмы Microsoft. Спецификация COM предназначена для создания стопроцентно переносимых программных компонентов, наделенных возможностью безопасного обновления. О COM можно рассказать довольно много, но эта книга посвящена другой теме. Мы рассмотрим COM лишь в объеме, необходимом для использования DirectDraw.

В COM используется объектно-ориентированная модель, более жесткая, чем модели, принятые в языках типа C++. Так, доступ к COM-объектам всегда осуществляется с помощью функций. COM-объекты не могут иметь открытых переменных. Кроме того, наследование в COM выглядит ограниченным по сравнению с C++.



Структура приложения


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


Классы DirectDrawWin и DirectDrawApp образуют «каркас» для приложений DirectDraw. Их главная цель — предоставить структурную основу приложения и поддержку основных функций, не скрывая от программиста всех подробностей. Ведь эта книга учит программировать для DirectDraw, а не пользоваться неким набором классов, который в свою очередь использует DirectDraw.

Итак, классы DirectDrawWin и DirectDrawApp образуют структурную основу приложения и реализуют поддержку его работы. «Структурная основа» означает, что, хотя всю главную работу придется выполнять вам, эти классы решают, когда и как это должно происходить. Например, класс DirectDrawWin позволяет выбрать исходный видеорежим. Для этого класс вызывает написанную вами функцию, которая просматривает список доступных видеорежимов и выбирает один из них. Эта функция вызывается классом DirectDrawWin в нужный момент.

Структурные классы также поддерживают основные функции приложения. Такая поддержка организуется в виде вспомогательных функций, упрощающих программирование. Например, класс DirectDrawWin содержит функцию для загрузки BMP-файла на поверхность. Это упрощает программу и позволяет эффективно использовать ранее написанный код.

Тем не менее, если вы собираетесь действительно глубоко изучить DirectDraw, нельзя, чтобы кто-то выполнял всю работу за вас. По этой причине код структурной основы включается в каждый проект; не существует центральной библиотеки классов, используемой во всех программах. Каждый проект, находящийся на CD-ROM или сгенерированный AppWizard, является законченным и вполне самостоятельным; он не зависит ни от чего, кроме MFC и DirectX. Вы сможете модифицировать, дополнять или удалять фрагменты структурного кода так, как сочтете нужным.




По своей структуре оконная версия приложения Bounce почти не отличается от полноэкранной. Как и прежде, классы DirectDrawWin и DirectDrawApp организуют поддержку DirectDraw и используются в качестве базовых для классов, относящихся к конкретным приложениям.




Программа Switch, как и все остальные программы в этой книге, была создана с помощью DirectDraw AppWizard, так что ее структура покажется вам знакомой, если вы прочитали главу 3. Реализация программы основана на двух классах, SwitchWin и SwitchApp, производных от классов DirectDrawWin и DirectDrawApp соответственно. Класс SwitchApp остался в том виде, в котором он был сгенерирован AppWizard, и потому дальнейшему обсуждению не подлежит.

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




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


Рис. 6.2. Программа Smear

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

Часть программы Smear, работающая с DirectDraw, отличается от всех остальных программ книги, потому что в данном случае не применяется переключение страниц — вместо этого мы непосредственно обновляем содержимое экрана. Такой подход имеет ряд последствий.

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

Во-вторых, мы уже не можем просто стереть фоновое изображение. В других программах мы стираем весь вторичный буфер, строим новый кадр и обновляем экран; никакого мерцания при этом не возникает. Стирание вторичного буфера в программе Smear вызовет заметное мерцание, потому что стертый фон будет отображаться во время вывода нового кадра. Программа названа Smear (то есть «размазывание») как раз потому, что фон не стирается, в результате при перемещении поверхности остается смазанный след.

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



Структура заголовка


Данные заголовка BMP-файла хранятся в двух структурах: BITMAPFILEHEADER и BITMAPINFOHEADER. Структура BITMAPFILEHEADER присутствует в начале любого BMP-файла и содержит информацию о самом файле. Для нас в этой структуре представляет интерес лишь одно поле — bfType, сигнатура BMP-файла (информацию об остальных полях можно найти в справочной системе Visual C++). В BMP-файлах это поле содержит буквы BM (обе буквы — прописные). По содержимому этого поля мы будем убеждаться в том, что выбранные файлы действительно имеют формат BMP.

Структура BITMAPINFOHEADER содержит информацию об изображении, хранящемся в BMP-файле. Эта структура объявляется так:


typedef struct tagBITMAPINFOHEADER{ DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;


Первое поле, biSize, определяет размер структуры BITMAPINFOHEADER в байтах. Если ваша программа создает BMP-файл, это поле заполняется тривиально — достаточно определить размер структуры функцией sizeof. Однако при чтении BMP-файла по содержимому этого поля приходится рассчитывать позицию файла, на которой структура заголовка кончается. Эта мера обеспечивает обратную совместимость, благодаря ей Microsoft в будущем сможет увеличить размер структуры BITMAPINFOHEADER, не нарушая работы существующих приложений.

СОВЕТ

Лучше молчать и прослыть глупцом…

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

Поля biWidth, biHeight и biBitCount определяют размеры изображения. Содержимое поля biCompression позволяет узнать, хранится ли изображение в сжатом виде. Поскольку мы не собираемся работать со сжатыми BMP-файлами, необходимо проверить, имеет ли это поле значение BI_RGB (а не BI_RLE8, свидетельствующее о сжатии файла).

В поле biSizeImage хранится размер графических данных (в пикселях). Однако учтите, что это поле часто оказывается незаполненным (содержит нулевое значение). В таких случаях нам придется самостоятельно вычислять размер графических данных.

Наконец, поле biClrUsed определят количество цветов в палитре (для палитровых изображений). Как и поле biSizeImage, оно часто может быть равно нулю. Это означает, что палитра содержит максимальное количество элементов (256 для 8-битных изображений). Остальные поля структуры BITMAPINFOHEADER не представляют для нас интереса, поэтому я не стану утомлять вас их обсуждением.



Структуры DirectDraw


После рассмотрения всех интерфейсов и функций DirectDraw мы переходим к структурам данных. Всего в DirectDraw определено восемь структур:

DDBLTFX

DDCAPS

DDOVERLAYFX

DDPIXELFORMAT

DDSURFACEDESC

DDSCAPS

DDBLTBATCH

DDCOLORKEY

С некоторыми из этих структур мы уже встречались. Например, структура DDCOLORKEY упоминалась при обсуждении функции SetColorKey() интерфейса DirectDrawSurface. В настоящем разделе мы не станем детально рассматривать каждую структуру, а вместо этого разберемся с одной особенностью DirectDraw, которая способна причинить немало бед, если о ней забыть.

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

Например, фрагмент для работы со структурой DDSURFACEDESC может выглядеть так:


DDSURFACEDESC surfdesc; surfdesc.dwSize = sizeof(surfdesc); surf->GetSurfaceDesc( &surfdesc );


Сначала мы объявляем структуру, затем присваиваем полю dwSize значение, используя функцию sizeof(). После этого структура передается функции GetSurfaceDesc() интерфейса DirectDrawSurface. Если забыть присвоить значение полю dwSize, вызов функции закончится неудачей.

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

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


DDSURFACEDESC surfdesc; ZeroMemory( &surfdesc, sizeof(surfdesc) ); surfdesc.dwSize = sizeof(surfdesc); surf->GetSurfaceDesc( &surfdesc );


Функция Win32 ZeroMemory() заполняет нулями область памяти, начало которой передается в качестве первого аргумента. Второй аргумент функции определяет размер инициализируемой области. Преимущество такого подхода состоит в том, что теперь можно выяснить, какие поля структуры изменились в результате вызова функции GetSurfaceDesc(). Если не инициализировать структуру, случайные значения в ее полях можно принять за величины, занесенные в нее DirectDraw.



Термины и концепции


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



Традиционная оптимизация


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

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

Не стоит просто предполагать, что некоторые части вашей программы работают медленно. Перед началом вскрытия расставьте таймеры в подозрительном фрагменте и воспользуйтесь профайлером Visual C++. Издевательства над невинными функциями не только ведут к напрасным затратам времени, но и повышают вероятность внесения новых ошибок.



Требования к читателю


Эта книга научит вас почти всему, что можно узнать о DirectDraw. Тем не менее она не учит C, C++, MFC или работе с Developer Studio пакетов семейства Visual — предполагается, что вы уже знакомы с этими темами. С другой стороны, от вас не требуется никаких выдающихся познаний. Например, мы будем использовать MFC, но лишь в объеме, необходимом для написания наших приложений. Следовательно, чтобы читать эту книгу, вовсе не обязательно быть экспертом в MFC.



Удаленная отладка


Процесс отладки упрощается, если вам повезло и в вашем распоряжении оказались два компьютера, объединенных в локальную сеть. В этом случае возможна удаленная отладка, при которой на одном компьютере работает отлаживаемая программа, а на другом — отладчик Visual C++. При удаленной отладке исчезают все проблемы, связанные с переключением страниц, палитрами и отображением отладчика. Отладчик всегда присутствует на экране и с ним можно работать, потому что отладчик и приложение работают на разных компьютерах. Даже если ваша программа «сломается», ее состояние можно будет просмотреть в отладчике. Поскольку удаленная отладка обладает такими преимуществами (и при этом так плохо документирована), мы поговорим о том, как происходит ее настройка. Если эта тема вас не интересует, можете пропустить этот раздел.

Для выполнения удаленной отладки вам понадобятся два компьютера, объединенных в локальную сеть. Конкретные параметры сети (количество узлов, тип кабеля, сетевые адаптеры и т. д.) не имеют значения; главное — чтобы на обоих компьютерах был установлен сетевой протокол TCP/IP. Кроме того, необходимо установить на обоих компьютерах Visual C++.

Затем вы должны выбрать, на каком компьютере будет работать отладчик Visual C++, а на каком — приложение. Мы будем называть компьютер, на котором работает Visual C++, хостовым компьютером (или хостом), а компьютер с отлаживаемым приложением — удаленным компьютером. Обычно хостом назначают более мощный из двух компьютеров.

Вам необходимо узнать адреса TCP/IP для обоих компьютеров. Проще всего получить этот адрес с помощью утилиты Winipcfg (WINIPCFG.EXE). Для Winipcfg не существует ярлыка (shortcut) или команды меню Start, поэтому придется либо создать ярлык или команду (утилита находится в каталоге Windows), либо запустить Winipcfg из окна DOS или диалогового окна Run меню Start. Окно утилиты Winipcfg изображено на рис. А.1. В поле IP Address приведено значение адреса TCP/IP . В нашем примере удаленный компьютер имеет адрес 128.128.128.1 (а хостовый компьютер будет иметь адрес 128.128.128.2).





Рис. А.1. Утилита Winipcfg

Также вам понадобится сетевое имя удаленного компьютера. Сетевое имя представляет собой неформальную строку, которая облегчает идентификацию сетевых компьютеров (при этом вам не придется запоминать адреса TCP/IP для всех компьютеров сети). Чтобы узнать сетевое имя компьютера, можно щелкнуть правой кнопкой мыши на значке Network Neighborhood (он находится на рабочем столе) и выбрать из контекстного меню команду Properties; откроется диалоговое окно Network. Перейдите на вкладку Identification. На рис. А.2 показано, как выглядит эта вкладка. В данном примере имя удаленного компьютера — remotemachine. Вероятно, вам не стоит менять сетевое имя, если только вы не являетесь сетевым администратором — просто воспользуйтесь именем, которое было назначено компьютеру при исходной настройке.

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



Рис. А.2. Диалоговое окно Network Identification

После того как вы создадите каталог для приложения и убедитесь в том, что оно может выполняться на удаленном компьютере, запустите отладочный монитор Visual C++ (MSVCMON.EXE). Для этой программы, как и для утилиты Winipcfg, не существует ярлыка, поэтому вам придется создать его самостоятельно. Отладочный монитор находится в каталоге bin Visual C++. Если вы собираетесь плотно заняться удаленной отладкой, вероятно, ярлык отладочного монитора следует поместить в удобное место.


Окно отладочного монитора Visual C++ изображено на рис. А.3.

Теперь нужно указать адрес хостового компьютера. Убедитесь, что в отладочном мониторе выбрано соединение TCP/IP, и нажмите кнопку Settings — откроется диалоговое окно Win32 Network (TCP/IP) Settings. Введите адрес хоста в первом поле. На рис. А.4 показано, как выглядит диалоговое окно с введенным адресом.

Вводить пароль необязательно — я рекомендую оставить это поле пустым. После ввода адреса хоста нажмите кнопку OK. Вы возвратитесь к диалоговому окну Debug Monitor. Нажмите кнопку Connect. Появится маленькое окно, которое показывает, что отладочный монитор готов начать сеанс отладки. Теперь удаленный компьютер настроен для проведения отладки.



Рис. А.3. Диалоговое окно Visual C++ Debug Monitor



Рис. А.4. Диалоговое окно Network Settings

Теперь займемся настройкой хоста. Запустите Visual C++ и загрузите проект, который вы собираетесь отлаживать. В нашем примере используется проект с именем Sample. Выполните команду Project|Settings и перейдите на вкладку General. На ней задается каталог, в котором генерируется итоговый выполняемый файл. Для упрощения отладки мы будем генерировать его прямо на удаленном компьютере — это избавит нас от необходимости заново копировать выполняемый файл на удаленный компьютер после очередной компиляции. В поле Output files введите имя каталога, созданного ранее на удаленном компьютере. Имя начинается с условного обозначения сетевого файла (\\). На рис. А.5 показано, как выглядит поле Output files с указанным каталогом.

Задаваемая строка начинается с имени удаленного компьютера (remotemachine), за которым следует имя диска (в нашем примере каталог удаленной отладки находится на диске C) и имя каталога. Перейдите на вкладку Debug; здесь необходимо изменить все три поля. Вы должны задать местонахождение целевого выполняемого файла и рабочий каталог. На рис. А.6 показано, как заполняются поля вкладки Debug в нашем примере.


Как и на рис. А.5, путь к выполняемому файлу включает имя удаленного компьютера.

Перед тем как приступать к удаленной отладке, остается сделать лишь один шаг. Выполните команду Debugger Remote Connection из меню Build. Эта команда открывает окно (похожее на окно отладочного монитора), предназначенное для активизации и настройки сеанса удаленной отладки. По умолчанию в списке Connection выбирается строка Local. Чтобы перейти к удаленной отладке, выберите строку Network (TCP/IP), как показано на рис. А.7.



Рис. А.5. Поле Output files подготовлено для генерации выполняемого файла на удаленном компьютере



Рис. А.6. Изменения на вкладке Debug



Рис. А.7. Окно Remote Connection

Нажмите кнопку Settings — появится окно, изображенное на рис. А.4. Задайте в нем адрес TCP/IP удаленного компьютера. Поле Debug monitor password оставьте пустым, если только вы не задали пароль при настройке отладочного монитора на удаленном компьютере.

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

Запустите отладчик. Если все параметры были настроены правильно, окно Debug Monitor на удаленном компьютере исчезнет — это говорит о том, что соединение установлено.

Удаленный компьютер попытается запустить программу, и вам будет предложено (на хостовом компьютере) указать местонахождение необходимых DLL. Для каждой DLL открывается окно Find Local Module. Вы должны ввести имя файла, включая сетевое имя компьютера. Окно Find Local Module изображено на рис. А.8 (в нем предлагается задать местонахождение файла WINMM.DLL). Часть введенной строки не поместилась на рисунке, но вы должны помнить о том, что кроме пути необходимо указать имя файла.



Рис. А.8. Окно Find Local Module

После того как вы укажете местонахождение всех DLL (как правило, они располагаются в каталоге windows\system), программа запускается на удаленном компьютере, однако ее отладочный вывод направляется в окно отладчика на хосте.


Теперь вы можете отлаживать свою программу. Мы настроили Visual C++ так, что при каждой компиляции новый выполняемый файл будет автоматически копироваться на удаленный компьютер. Удаленная отладка почти ничем не отличается от обычной, разве что программа выполняется не на хосте, а на другом компьютере.

Напоследок я скажу еще несколько слов об удаленной отладке. Во-первых, как ее отключить? Хороший вопрос. Выполните команду Build|Debugger Remote Connection в Visual C++ (на хосте), затем выберите строку Local в списке Connection и нажмите кнопку OK.

Удаленная отладка обладает многими достоинствами, но она не идеальна. Прежде всего, точки прерывания в ней работают ненадежно, а то и вовсе не работают. Другая проблема заключается в том, что после настройки удаленной отладки и выбора местонахождения DLL, необходимых для вашей программы, Visual C++ будет настаивать на загрузке DLL даже после возврата в режим локальной отладки. Следовательно, в конце раздела, посвященного удаленной отладке, мне следует рассказать о том, как вернуться к нормальной загрузке DLL.

Выполните команду Project|Settings и перейдите на вкладку Debug. Выберите из списка Category строку Additional DLL. Открывается список DLL, которые Visual C++ пытается загрузить при отладке вашей программы (независимо от того, включена удаленная отладка или нет). Чтобы отказаться от переназначения DLL (для нормальной отладки вам не потребуется ни один из элементов этого списка), выделите соответствующую строку и снимите флажок в левом верхнем углу. Список Additional DLLs с некоторыми DLL, необходимыми для типичных приложений MFC, изображен на рис. А.9.



Рис. А.9. Окно Additional DLLs


Управление версией DirectInput


По умолчанию приложения DirectInput требуют, чтобы версия runtime-части DirectX, установленной на компьютере пользователя, совпадала с версией SDK, использованной для компиляции приложения, или превышала ее. Например, если вы создаете приложение с использованием DirectX 5 и запускаете его на компьютере с установленным DirectX 3, вызов функции DirectInputCreate() закончится неудачей.

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

Обратную совместимость можно обеспечить двумя способами. Вы можете либо откомпилировать свое приложение для старой версии DirectX SDK, либо распорядиться, чтобы SDK эмулировал старую версию. В приложениях DirectInput этой книги используется второй способ.

Переопределяя стандартную константу DIRECTINPUT_VERSION, программа сообщает заголовочным файлам DirectInput о том, что необходимо обеспечить совместимость с DirectX 3. Это делается так:


#include <AfxWin.h> #include <AfxExt.h> #include <AfxTempl.h> #include <fstream.h> #include <math.h> #include <ddraw.h> #define DIRECTINPUT_VERSION 0x0300 #include <dinput.h>


В приведенном фрагменте присутствуют директивы include для всех заголовочных файлов программы. Символическая константа DIRECTINPUT_VERSION должна быть определена до включения файла dinput.h (номер версии определяется старшим байтом).

В результате программа будет компилироваться как версией 3, так и версией 5 DirectX SDK (Microsoft пропустила версию DirectX 4). Получившиеся выполняемые файлы будут работать с runtime-частью DirectX 3 и выше. Поскольку Windows NT Service Pack 3 включает поддержку DirectX 3 для DirectInput, программы успешно компилируются и работают в Windows NT.



Уровни кооперации


Чтобы приложение могло задать нужную степень контроля над устройством, в DirectInput используются уровни кооперации. DirectInput, как и DirectDraw, позволяет установить монопольный (exclusive) и совместный (nonexclusive) режим доступа для каждого устройства. Если приложение DirectInput обладает монопольным доступом к устройству ввода, то никакое другое приложение заведомо не сможет получить монопольного доступа к тому же устройству (хотя сможет получить совместный доступ).

DirectInput также позволяет задать уровень кооперации для активного (foreground) и фонового (background) режимов работы — эти два термина часто приводят к недоразумениям. Активный доступ (foreground access) означает, что приложение работает с устройством только тогда, когда обладает фокусом ввода (по аналогии с тем, как ввод с клавиатуры по умолчанию передается приложению, обладающему фокусом). Фоновый доступ (background access) означает, что приложение может обращаться к устройству независимо от того, обладает ли оно фокусом ввода. Интуитивно кажется, будто активный доступ обладает большими возможностями, но на самом деле это не так. В программах Qwerty и Smear используется совместный активный уровень кооперации.



Варианты VisualC++


И последнее замечание о Visual C++. Поскольку этот продукт выпускается в трех вариантах (Learning, Professional и Enterprise), иногда читателям не удается скомпилировать проект. Для написания и тестирования программ этой книги я пользовался вариантом Professional, так что при работе с другими вариантами могут возникнуть проблемы. Впрочем, в основном это проблемы мелкие и легко излечимые. Если у вас возникнут трудности и вы решите обратиться ко мне, пожалуйста, укажите версию и вариант Visual C++.



VFW API


Имена многих функций VFW API начинаются с букв AVI. Другие функции (например, относящиеся к сжатию) начинаются с IC. Существуют некоторые исключения, но большинство функций VFW снабжается одним из этих префиксов. Алфавитный список всех функций VFW приведен в табл. 8.1.

Таблица 8.1. Функции VFW

AVIBuildFilter()
AVICLearClipboard()
AVIFileAddRef()
AVIFileCreateStream()
AVIFileEndRecord()
AVIFileExit()
AVIFileGetStream()
AVIFileInfo()
AVIFileInit()
AVIFileOpen()
AVIFileReadData()
AVIFileRelease()
AVIFileWriteData()
AVIGetFromClipboard()
AVIMakeCompressedStream()
AVIMakeFileFromStream()
AVIMakeStreamFromClipboard()
AVIPutFileOnClipboard()
AVISave()
AVISaveOptions()
AVISaveOptionsFree()
AVISaveV()
AVIStreamAddRef()
AVIStreamBeginStreaming()
AVIStreamCreate()
AVIStreamEndStreaming()
AVIStreamCreate()
AVIStreamEndStreaming()
AVIStreamFindSample()
AVIStreamGetFrame()
AVIStreamGetFrameClose()
AVIStreamGetFrameOpen()
AVIStreamInfo()
AVIStreamLength()
AVIStreamOpenFromFile()
AVIStreamRead()
AVIStreamReadData()
AVIStreamReadFormat()
AVIStreamRelease()
AVIStreamSampleToTime()
AVIStreamSetFormat()
AVIStreamStart()
AVIStreamTimeToSample()
AVIStreamWrite()
AVIStreamWriteData()

CreateEditableStream()
EditStreamClone()
EditStreamCopy()
EditStreamCut()
EditStreamPaste()
EditStreamSetInfo()
EditStreamSetName()
ICClose()
ICCompress()
ICCompressorChoose()
ICCompressorFree()
ICDecompress()
ICDecompressEx()
ICDecompressExBegin()
ICDecompressExQuery()
ICDraw()
ICDrawBegin()
ICDrawSuggestFormat()
ICGetInfo()
ICGetDisplayFormat()
ICImageCompress()
ICImageDecompress()
ICInfo()
ICInstall()
ICLocate()
ICOpen()
ICOpenFunction()
ICRemove()
ICSendMessage()
ICSeqCompressFrame()
ICCompressFrameEnd()
ICCompressFrameStart()
ICGetStatusProc()
MyStatusProc()


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



СОВЕТ

Не забудьте включить VFW в проект

Перед тем как пользоваться функциями Video For Windows, необходимо включить в проект заголовочный файл vfw.h и добавить vfw32.lib в список файлов компоновщика.

Для правильной работы функций из табл. 8.1 необходимо инициализировать VFW функцией AVIFileInit(). Эта функция не получает аргументов и не возвращает никакого значения, поэтому работать с ней проще простого.

После инициализации VFW можно создавать поток функцией AVIStreamOpenFromFile(). Эта функция получает в одном из аргументов имя AVI-файла и инициализирует по нему логический номер потока. Полученный логический номер затем используется как аргумент большинства функций VFW и определяет поток, с которым выполняется операция.

В частности, по логическому номеру, возвращаемому AVIStreamOpenFromFile(), можно получить сведения о видеоролике. Функция AVIStreamReadFormat() сообщает такие данные, как количество кадров, размер изображения и глубина пикселей; для этого она заполняет структуру BITMAPINFOHEADER (возможно, структура BITMAPINFOHEADER

покажется вам знакомой — мы уже встречались с ней при описании BMP-файлов). Я снова приведу определение этой структуры, взятое из файла Windows wingdi.h:

typedef struct tagBITMAPINFOHEADER{ DWORD biSize; LONG biWidth; LONG biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; LONG biXPelsPerMeter; LONG biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant; } BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

Некоторые поля структуры (например, biXPelsPerMeter и biYPelsPerMeter) не имеют отношения к AVI-файлам, но таких полей немного. Размеры, глубина пикселей, алгоритм сжатия и количество цветов — все это можно взять из структуры BITMAPINFOHEADER после успешного вызова функции AVIStreamReadFormat().

Более подробные сведения о потоке можно получить с помощью функции AVIStreamInfo(). Функция AVIStreamInfo(), как и функция AVIStreamReadFormat(), получает в качестве аргумента логический номер и заполняет структуру данными о потоке.


Для передачи информации AVIStreamInfo() использует структуру AVISTREAMINFO. Ее определение в файле vfw.h выглядит так:

typedef struct _AVISTREAMINFOW { DWORD fccType; DWORD fccHandler; DWORD dwFlags; DWORD dwCaps; WORD wPriority; WORD wLanguage; DWORD dwScale; DWORD dwRate; DWORD dwStart; DWORD dwLength; DWORD dwInitialFrames; DWORD dwSuggestedBufferSize; DWORD dwQuality; DWORD dwSampleSize; RECT rcFrame; DWORD dwEditCount; DWORD dwFormatChangeCount; WCHAR szName[64]; } AVISTREAMINFOW, FAR * LPAVISTREAMINFOW;


Давайте рассмотрим некоторые ключевые поля этой структуры. Первое поле, fccType, определяет тип потока. В файлах формата AVI поддерживаются четыре типа потоковых данных: видео, аудио, MIDI (музыка) и текст. В этой книге будут рассматриваться только видеопотоки.

Второе поле структуры AVISTREAMINFO, fccHandler, определяет алгоритм сжатия, примененный при сохранении видеопотока. Как вы вскоре увидите, в Video For Windows это поле можно использовать для создания «декомпрессоров», которые способны восстановить каждый сжатый кадр из потока.

Поле dwStart определяет индекс первого кадра потока (это важно, потому что первый кадр видеоролика может иметь индекс 0 или 1). Эту же величину можно непосредственно получить функцией AVIStreamStart().В поле dwLength хранится общее количество кадров видеоролика. Его можно либо взять из структуры AVISTREAMINFO, либо получить с помощью функции AVIStreamLength().

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

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

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

После завершения работы с потоком необходимо закрыть его функцией AVIStreamRelease(). После вызова функции AVIStreamRelease()

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


Video For Windows


Для создания и чтения AVI-файлов Windows предоставляет разработчикам Video For Windows (VFW) API. Этот API заметно упрощает задачу воспроизведения видео, поскольку в нем имеются удобные средства для поиска и извлечения кадров из ролика.

VFW не пытается поддерживать все возможные алгоритмы сжатия, используемые для хранения AVI-данных. Вместо этого VFW допускает установку и удаление компрессоров и декомпрессоров, обеспечивающих работу одного или нескольких алгоритмов сжатия. Таким образом, VFW позволяет выбрать нужные компрессор и декомпрессор в зависимости от потребностей разработчика. Кроме поддержки сжатия и распаковки VFWAPI предоставляет средства для поиска и извлечения кадров, а также для решения других задач, связанных с потоками.



Видеорежимы


Видеорежимом называется набор параметров, поддерживаемый аппаратурой видеокарты (видеоадаптера) и позволяющий организовать вывод графического изображения. Самым известным атрибутом видеорежима является разрешение экрана. По умолчанию в Windows используется видеорежим с разрешением 640x480. Это означает, что на экране выводится 640 пикселей по горизонтали и 480 пикселей по вертикали. Также часто встречаются видеорежимы с разрешением 800x600 и 1024x768. Некоторые видеокарты поддерживают так называемые режимы ModeX. Типичный режим ModeX имеет разрешение 320x200.

Видеорежимы также различаются по глубине пикселей (pixel depth). Этот параметр определяет количество различных значений, принимаемых отдельным пикселем, и, следовательно, количество отображаемых цветов. Например, в видеорежиме с глубиной пикселей в 8 бит каждый пиксель может иметь один из 256 различных цветов. В режимах с 16-битной глубиной пикселей поддерживается отображение до 65 536 цветов. Глубина пикселей обычно равна 8, 16, 24 или 32 битам.

Видеорежимы реализуются специальным устройством, установленным на компьютере, - видеокартой. На видеокарте устанавливается отдельная память, не входящая в основную память компьютера. Память, установленную на видеокарте, мы будем называть видеопамятью, а обычную память (RAM) - системной памятью. Объем памяти, необходимой для поддержки определенного видеорежима, определяется разрешением и глубиной пикселей в этом режиме. Например, для видеорежима 640x480x8 (640x480 пикселей, каждый из которых имеет глубину в 8 бит) требуется 307 300 байт. Видеорежим 1024x768x16 требует 1 572 864 байт. Для поддержки видеорежимов используется видеопамять. Следовательно, режимы, поддерживаемые конкретной видеокартой, ограничиваются объемом установленной видеопамяти. Скажем, режим 1024x768x16 не поддерживается видеокартами с 1 Мбайт памяти, потому что для него такой объем памяти недостаточен.

Одна из ключевых возможностей DirectDraw — переключение видеорежимов. Она позволяет приложению DirectDraw обнаружить и активизировать любой режим, поддерживаемый установленной видеокартой. Переключение режимов подробно рассматривается в главе 4.



Visual C++


После установки SDK необходимо сообщить Visual C++ о его местонахождении. По умолчанию SDK инсталлируется в каталог с именем dxsdk/sdk. Заголовочные файлы помещаются в каталог dxsdk/sdk/inc, а библиотечные — в каталог dxsdk/sdk/lib.

Существуют два способа уведомить Visual C++ о местонахождении SDK. Вы можете либо указывать полный файловый путь при использовании файлов, либо включить нужные каталоги в путь поиска Visual C++. Желательно использовать второй метод, для чего применяется диалоговое окно, вызываемое командой Tools | Options | Directories. На рис. 1.1 показано, как выглядит это диалоговое окно после добавления пути к каталогу с заголовочными файлами.


Рис. 1.1. Диалоговое окно Visual C++ с перечнем каталогов

Вам также придется включить в список каталог dxsdk/sdk/lib. Это делается практически так же, как и с каталогом dxsdk/sdk/inc.

На рисунке каталог DirectX был с помощью кнопок ­ и Ї помещен в начало списка, над стандартными каталогами Visual C++. Это имеет значение, если от версии DirectX SDK, входящей в Visual C++, вы перейдете к более свежей. В противном случае будет использоваться старая версия (Visual C++ просматривает каталоги из списка сверху вниз).

После выполнения всех описанных действий вы сможете компилировать программы DirectX. Тем не менее осталась еще одна потенциальная проблема. Чтобы в DirectX 2 SDK нормально проходила компоновка программ, вызывающих функцию QueryInterface() для GUID интерфейсов DirectX, необходимо определить символическую константу INITGUID. Этот символ должен быть определен в одном и только одном исходном файле, и притом до самой первой директивы #include, как показано в следующем фрагменте:


#define INITGUID #include <ddraw.h> // ... остальные директивы include ...


В DirectX 3 и выше этот вариант тоже работает, однако появляется и более элегантное решение. Вместо того чтобы определять символ INITGUID, включите в проект файл dxguid.lib (в диалоговом окне, вызываемом командой Build | Settings).



Восстановление потерянных поверхностей


Прежде чем расставаться с программой Bumper, мы должны посмотреть, как происходит восстановление потерянных поверхностей. Как обычно, для этого служит функция RestoreSurfaces():


void BumperWin::RestoreSurfaces() { for (int i=0;i<nsprites;i++) sprite[i]->GetSurf()->Restore();

LoadSurface( *sprite[0], "diamond.bmp"); LoadSurface( *sprite[1], "diamond.bmp");

LoadSurface( *sprite[2], "triangle.bmp" ); LoadSurface( *sprite[3], "triangle.bmp" );

LoadSurface( *sprite[4], "rect.bmp" ); LoadSurface( *sprite[5], "rect.bmp" );

LoadSurface( *sprite[6], "oval.bmp" ); LoadSurface( *sprite[7], "oval.bmp" );

text->Restore(); LoadSurface( text, "text.bmp" ); }


Сначала область памяти каждой поверхности восстанавливается функцией Restore() (если поверхность не была потеряна, вызов Restore() игнорируется). Затем функция LoadSurface() восстанавливает содержимое поверхности. Обратите внимание — здесь, как и в функции DrawScene(), используется оператор LPDIRECTDRAWSURFACE(), позволяющий передавать объекты Sprite вместо указателей на поверхности. Работа функции завершается восстановлением поверхности меню (text).



Восстановление поверхностей


Наше приложение благополучно инициализируется и выводит графические данные. Теперь необходимо справиться с возможной потерей поверхностей. При рассмотрении функции DirectDrawWin::PreDrawScene мы видели, что DirectDrawWin вызывает виртуальную функцию RestoreSurfaces(), чтобы производный класс получил возможность восстановить потерянные поверхности. Функция RestoreSurfaces() отвечает за восстановление как потерянной памяти поверхности, так и ее содержимого. Функция BounceWin::RestoreSurfaces() выглядит так:


void BounceWin::RestoreSurfaces() { if (surf1->IsLost()==FALSE) return; CString filename; if (GetCurDisplayDepth()==8) filename="tri08.bmp"; else filename="tri24.bmp";

surf1->Restore(); LoadSurface( surf1, filename ); }


DirectDraw может отнимать у неактивного приложения только поверхности, находящиеся в видеопамяти, так что нет смысла в восстановлении поверхностей из системной памяти. Поэтому RestoreSurfaces() сначала проверяет, была ли потеряна единственная вспомогательная поверхность нашего приложения, и если нет — функция прекращает работу. Если же поверхность была потеряна, мы восстанавливаем ее память функцией Restore(), а содержимое — функцией LoadSurface().


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

Обратите внимание: мы не восстанавливаем значение, отображаемое на поверхности FPS. Потеря первичной поверхности и вторичного буфера означает, что последнее вычисленное значение FPS, по всей видимости, стало неверным. Наша программа было свернута или уступила фокус другому приложению; в любом случае нельзя с уверенностью сказать, насколько быстро обновляется изображение в данный момент. Вместо того чтобы выводить устаревшее значение FPS, мы очищаем поверхность и присваиваем переменной displayfps значение FALSE, тем самым запрещая вывод поверхности до получения новой величины FPS. Кроме того, мы обнуляем переменную framecount, чтобы перезапустить механизм вычисления FPS.

Вот и все, что касается программы Switch. Пора заняться частотой смены кадров и перейти к приложению SuperSwitch.




Засидевшись допоздна над своей предыдущей


Засидевшись допоздна над своей предыдущей книгой, «Cutting-Edge Direct3D Programming», я решил ложиться спать. Оказалось, что та же самая мысль пришла в голову здоровенному пауку-каракурту. Что еще хуже, для этого нами была выбрана одна и та же кровать. Мы мирно улеглись, не подозревая о присутствии друг друга (во всяком случае, я ничего не подозревал).

До того мне приходилось слышать о каракуртах и даже видеть их время от времени. Бесспорно, они пользуются дурной славой. Я полагал, что после укуса каракурта следуют пять или десять минут невыносимой боли, а затем неизбежно наступает смерть. Оказалось, я ошибался в обоих отношениях. Сам по себе укус проходит совершенно безболезненно, но если его не лечить, боль продолжается до 48 часов. Тем не менее укус каракурта (даже без лечения) редко бывает смертельным.

И мне, и пауку пришлось пожалеть о встрече. Пауку — потому что он был жестоко раздавлен своей разгневанной и удивленной жертвой. Мне — потому что у врача я оказался лишь через восемь часов после укуса (а серьезные мышечные спазмы начинаются через 2–4 часа). После нескольких посещений местной амбулатории проблема была решена. Этой ночью я так и не спал, и к тому же в течение восьми часов мучался от жуткой боли. В довершение всего перед вводом противоядия меня накачали валиумом. Разумеется, о работе не могло быть и речи.

При первой возможности я позвонил своему редактору, Скотту Палмеру (Scott Palmer). Заплетающимся языком я рассказал, что немного задержусь с очередной главой, потому что меня укусил каракурт. Скотт проявил полное понимание, и мы перенесли срок сдачи материала.

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

Скотт заверил меня, что он никогда не сомневался в моей искренности, а в список включил сразу все оправдания, независимо от того, поверил он или нет.
Тем не менее полагаю, его позиция вполне ясна.

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

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

Другими словами, Coriolis отвечает за распространение и продажу книги, однако за содержащийся в ней материал отвечаю я. Следовательно, если у вас возникнут какие-либо вопросы по поводу программ или CD-ROM, не стесняйтесь и пишите мне. Некоторые читатели обращаются в Coriolis, но издательство все равно просто пересылает почту мне. Если вы захотите обратиться ко мне, обязательно укажите, о какой книге идет речь, и постарайтесь сделать свои вопросы и замечания по возможности конкретными. Со мной можно связаться по адресу stan@rezio.com.

Кроме того, некоторые вопросы встречаются особенно часто. Я собираюсь создать и вести список ЧаВО (часто задаваемых вопросов) на Web-узле этой книги (www.rezio.com/wgp). Здесь вы найдете ответы на некоторые вопросы, а также исправления ошибок, обновления и, возможно — даже новые демонстрационные программы.

Выбор драйвера


После того как все драйверы DirectDraw будут перечислены, функция OnCreate() выбирает один из них. Выбор драйвера по умолчанию может быть переопределен в производных классах с помощью виртуальной функции SelectDriver(). Возвращаясь к листингу 3.1, мы видим, что величина, возвращаемая функцией SelectDriver(), используется в качестве индекса массива driver (причем значения индекса начинаются с нуля). Индекс показывает, какой GUID (и, следовательно, драйвер) должен использоваться для инициализации DirectDraw. Версия SelectDriver() из класса DirectDrawWin выглядит так:


virtual int SelectDriver() { return 0; }


По умолчанию SelectDriver() возвращает 0, тем самым показывая, что должно использоваться первичное видеоустройство. Чтобы изменить ее поведение, следует переопределить SelectDriver() в классе, производном от DirectDrawWin. В нашем примере класс BounceWin переопределяет SelectDriver() так, чтобы в случае обнаружения нескольких драйверов выводилось меню:


int bounceWin::SelectDriver() { int numdrivers=GetNumDrivers(); if (numdrivers==1) return 0;

CArray<CString, CString> drivers; for (int i=0;i<numdrivers;i++) { LPSTR desc, name; GetDriverInfo( i, 0, &desc, &name ); drivers.Add(desc); }

DriverDialog dialog; dialog.SetContents( &drivers ); if (dialog.DoModal()!=IDOK) return -1;

return dialog.GetSelection(); }


Эта функция сначала определяет количество обнаруженных драйверов с помощью функции GetNumDrivers(), которая просто возвращает значение закрытой переменной totaldrivers. Если в системе обнаружен всего один драйвер, выводить меню незачем, поэтому функция возвращает 0, чтобы использовался первичный драйвер.

Если в системе доступно несколько драйверов, функция SelectDriver() создает меню, состоящее из строк с описаниями драйверов. Класс DriverDialog (простой класс диалогового окна, сгенерированный ClassWizard) выводит диалоговое окно, в котором пользователь может выбрать нужный драйвер. На рис. 3.9 изображено такое окно с двумя строками: первичным драйвером и драйвером DirectX 2 для видеокарты Orchid Righteous 3D.

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



Выбор и отображение BMP-файла


Функция ShowDialog() вызывается при запуске приложения и при выборе нового файла. ShowDialog() подготавливает DirectDraw к отображению диалогового окна, выводит окно, получает информацию о выбранном BMP-файле и выбранном видеорежиме и отображает содержимое файла. Функция ShowDialog() приведена в листинге 5.7.

Листинг 5.7. Функция ShowDialog()


void BmpViewWin::ShowDialog() { CRect displayrect=GetDisplayRect(); if (displayrect.Width()<640) ddraw2->SetDisplayMode( 640, 480, 8, 0, 0 );

if (GetDisplayDepth()==8) { ClearSurface( backsurf, 0 ); primsurf->SetPalette( syspal ); } else { BltSurface( backsurf, bmpsurf, x, y ); }

ddraw2->FlipToGDISurface(); ShowCursor(TRUE);

if (bmpdialog==0) { bmpdialog=new BmpDialog(); bmpdialog->SetArrays( &palettemode, &nonpalettemode ); }

if (bmpdialog->DoModal()==IDCANCEL) { PostMessage( WM_CLOSE ); return; }

fullfilename=bmpdialog->fullfilename; filename=bmpdialog->filename; pathname=bmpdialog->pathname;

int index=bmpdialog->GetIndex(); DWORD w,h,d; if (bmpdialog->FilePalettized()) { w=palettemode[index].w; h=palettemode[index].h; d=palettemode[index].d; } else { w=nonpalettemode[index].w; h=nonpalettemode[index].h; d=nonpalettemode[index].d; }

if (GetDisplayDepth()==8) primsurf->SetPalette( palette );

ActivateDisplayMode( GetDisplayModeIndex( w, h, d) );

LoadBmp();

ShowCursor(FALSE); }


Функция ShowDialog() прежде всего проверяет, что текущий видеорежим имеет разрешение не менее 640x480. Из обсуждения функции SelectInitialDisplayMode() нам известно, что при инициализации приложения это условие заведомо выполняется, однако функция ShowDialog() также вызывается при каждом отображении BMP-файла. Если в данный момент установлен режим низкого разрешения, то перед тем, как продолжать, мы переходим в режим 640x480x8. Это обусловлено тем, что режимы низкого разрешения часто являются режимами Mode X, а GDI в таких режимах не может правильно отображать диалоговые окна.

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

СОВЕТ

Диалоговое окно и изображение

Чтобы организовать совместный вывод текущего изображения и диалогового окна в палитровом видеорежиме, вам придется сократить 256 элементов палитры изображения до 236, добавить новые цвета в середину палитры (системные цвета занимают по 10 элементов в начале и в конце палитры) и пересчитать пиксели изображения в соответствии с внесенными изменениями. Обычно это ведет к снижению качества изображения, но присутствие диалогового окна все равно отвлекает внимание пользователя. Чтобы восстановить прежнее изображение, необходимо сохранить предыдущие варианты изображения и палитры.

Вызов функции FlipToGDISurface() гарантирует, что вывод GDI будет присутствовать на экране. Кроме того, мы включаем курсор мыши (отключенный при запуске приложения классом DirectDrawWin), чтобы для работы с диалоговым окном можно было пользоваться мышью.

Далее мы создаем экземпляр класса BmpDialog, если он не был создан ранее. Класс-оболочка BmpDialog создается ClassWizard, он предназначен для отображения диалогового окна и работы с ним. Класс содержит код для работы с управляющими элементами окна и реакции на действия пользователя. Код класса BmpDialog здесь не рассматривается, так как он не имеет никакого отношения к DirectDraw.

Обратите внимание: при создании диалогового окна мы вызываем функцию SetArrays() и передаем ей массивы palettemode и nonpalettemode в качестве аргументов. Эта функция передает диалоговому окну информацию о видеорежимах, предназначенных для отображения как палитровых, так и беспалитровых изображений.

Диалоговое окно отображается функцией DoModal(). Пользователь сможет нажать кнопку Display лишь после того, как будет выбран BMP-файл и видеорежим. При этом мы сохраняем имя и путь выбранного BMP-файла и определяем параметры выбранного видеорежима.


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

Наконец, функция ActivateDisplayMode() активизирует выбранный видеорежим, функция LoadBmp() загружает содержимое BMP-файла, а курсор мыши отключается.

Чтобы лучше понять, как происходит загрузка файла, необходимо рассмотреть функцию LoadBmp(), которая не только загружает BMP-файл, но и инициализирует механизм прокрутки. Функция LoadBmp() приведена в листинге 5.8.

Листинг 5.8. Функция LoadBmp()

BOOL BmpViewWin::LoadBmp() { CWaitCursor cur;

LPDIRECTDRAWSURFACE surf; surf=CreateSurface( filename, TRUE ); if (surf) { if (bmpsurf) bmpsurf->Release(); bmpsurf=surf; } else { TRACE("failed to load new file\n"); return FALSE; }

displayrect=GetDisplayRect(); TRACE("display: %d %d\n", displayrect.right, displayrect.bottom); GetSurfaceRect( bmpsurf, bmprect ); TRACE("surface: %d %d\n", bmprect.right, bmprect.bottom);

int mx = displayrect.Width()-bmprect.Width(); if (mx<0) { xscroll=TRUE; xlimit=mx; x=0; } else { xscroll=FALSE; x=mx/2; }

int my = displayrect.Height()-bmprect.Height(); if (my<0) { yscroll=TRUE; ylimit=my; y=0; } else { yscroll=FALSE; y=my/2; }

update_screen=TRUE;

return TRUE; }


Сначала функция LoadBmp() создает объект MFC CWaitCursor, чтобы на время ее работы на экране отображался курсор Windows в виде песочных часов. Затем она вызывает функцию CreateSurface() и передает ей в качестве аргумента имя выбранного BMP-файла. Реализация CreateSurface() рассматривалась ранее в этой главе, поэтому мы знаем, что эта функция загружает указанный BMP-файл на новую поверхность.

Затем LoadBmp() определяет параметры новой поверхности и текущий активный видеорежим и использует полученные данные для инициализации переменных класса BmpViewWin, связанных с прокруткой и позиционированием поверхностей. Если размеры поверхности меньше размеров видеорежима, поверхность центрируется на экране; если поверхность больше, следует разрешить ее прокрутку.Переменные x и y определяют текущую позицию на поверхности, а переменные xlimit и ylimit ограничивают диапазон прокрутки. Логические переменные xscroll и yscroll показывают, разрешена ли горизонтальная и вертикальная прокрутка поверхности.

Наконец, логической переменной update_screen присваивается значение TRUE; оно говорит о том, что функция DrawScene() должна обновить первичную поверхность. О функции DrawScene() речь пойдет в следующем разделе.




Выбор видеорежима


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


int BounceWin::SelectInitialDisplayMode() { int i, nummodes=GetNumDisplayModes(); DWORD w,h,d;

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (w==desiredwidth && h==desiredheight && d==desireddepth) return i; }

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (d==desireddepth) return i; }

return 0; }


Функция сначала определяет количество режимов функцией GetNumDisplayModes(), а затем в цикле пытается найти видеорежим с заданным разрешением и глубиной пикселей. Атрибуты каждого видеорежима извлекаются функцией GetDisplayModeDimensions(); если совпадение будет найдено, возвращается индекс видеорежима. В противном случае другой цикл ищет любой видеорежим с заданной глубиной пикселей. Поскольку цикл начинается с начала массива displaymode, с большей вероятностью будут выбираться режимы низкого разрешения. Если не найдено ни одного видеорежима с заданной глубиной пикселей, возвращается значение 0 — оно говорит о том, что следует использовать видеорежим с минимальным разрешением. Код возврата –1 сообщает DirectDrawWin о том, что ни один приемлемый видеорежим так и не был найден и работу приложения следует завершить.



Вычисление FPS


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

Чтобы вычислить FPS во время работы приложения, необходимо производить периодические измерения. К сожалению, скорость работы приложения может изменяться в зависимости от сложности графического вывода и объема внутренних вычислений. Кроме того, Windows замедляет работу приложения при передаче части процессорного времени другим приложениям или при переносе содержимого памяти на диск. В момент запуска приложение обычно работает медленнее, потому что Windows начинает сбрасывать данные на диск. Только после того, как Windows «успокоится» и перестанет работать с диском, можно будет получить достоверные значения FPS.

В программе Switch мы будем периодически вычислять FPS и отображать результат до истечения следующего интервала. Если запустить программу Switch, вы заметите, что значение FPS появляется не сразу; это происходит из-за того, что для получения достоверных показаний должно пройти некоторое время.

Для хронометража используется функция Win32 timeGetTime(), которая возвращает количество миллисекунд, прошедших с момента запуска Windows. В программе Switch функция timeGetTime() вызывается после каждых 100 кадров; значение FPS равно 100, разделенному на количество прошедших секунд.

Функция timeGetTime() не обеспечивает максимальной точности измерений, которую можно получить в Windows (для более точного хронометража можно воспользоваться функцией QueryPerformanceCounter()). Если бы мы отслеживали очень короткие периоды времени (например, интервалы по 10 миллисекунд), то функция timeGetTime() не давала бы приемлемых результатов, но поскольку таймер используется не чаще одного раза в секунду, подходит и timeGetTime().



Вывод текста


Самый простой способ вывести текст на поверхность — получить у Windows DC (контекст устройства) с помощью функции GetDC() интерфейса DirectDrawSurface. После этого можно вывести текст на поверхность стандартными функциями TextOut() и TextOutExt() и освободить DC функцией DirectDrawSurface::ReleaseDC(). Атрибуты текста (цвет, фон и способ вывода) выбираются следующими функциями Win32:

SelectObject()

SetBkMode()

SetBkColor()

SetTextColor()

Мы воспользуемся этими функциями в программе Switch для вывода меню видеорежимов и значения FPS.



если вы еще не успели


Первый шаг в великой битве с багами — переход на Windows NT ( если вы еще не успели этого сделать). Разумеется, вам не придется отказываться от поддержки Windows 95, но для разработки NT подходит лучше, а приложения DirectX переносятся из NT в 95 без перекомпиляции. Просто Windows NT работает более устойчиво, чем Windows 95. NT редко (очень редко) требует перезагрузки, тогда как в 95 это вполне обычное явление.

Оборотная сторона заключается в том, что по возможностям DirectX NT часто отстает от 95. Поскольку в Windows NT нельзя установить стандартную runtime-часть DirectX, на поддержку новых возможностей у Microsoft уходит больше времени. Следовательно, если вам необходимы новейшие возможности DirectX, то для разработки неизбежно придется использовать Windows 95. Для DirectDraw это не вызывает особых проблем, потому что в последних версиях эта библиотека практически не изменялась. А вот другие DirectX API, напротив, заметно изменились в DirectX 5 (особенно это относится к DirectInput и DirectPlay).

Профессиональные разработчики для DirectX отдают явное предпочтение NT. Более того, некоторые будущие бета-версии DirectX могут предназначаться только для NT.


Эта книга посвящена разработке приложений


Эта книга посвящена разработке приложений DirectDraw для Windows NT и Windows 95. Приступая к работе над демонстрационными программами, я заранее приготовился к раздражающим мелким отличиям и проявлениям несовместимости. Признаюсь, я был приятно удивлен.

И все же отличия существуют, особенно в части видеорежимов. Во-первых, Windows NT не поддерживает видеорежимы семейства Mode X. Независимо от того, поддерживаются ли такие видеорежимы установленной видеокартой, функция EnumDisplayModes() не сообщает о них, а функция SetDisplayMode() не активизирует их.

Во-вторых, Windows 95 позволяет задавать параметры монитора, а Windows NT — нет. В Windows 95 список видеорежимов, обнаруженных DirectDraw, учитывает возможности как видеокарты, так и монитора. Если режим поддерживается только видеокартой, но не монитором, DirectDraw не включает его в список. Однако в Windows NT параметры монитора не задаются; следовательно, DirectDraw выведет список всех видеорежимов, поддерживаемых видеокартой, независимо от того, реализуются ли они установленным монитором.

Следовательно, к переключению режимов в приложениях DirectDraw следует подходить с осторожностью. В коммерческих приложениях должен присутствовать механизм страховки, который бы позволял проверить видеорежим перед тем, как переходить в него. Подобный механизм используется при выборе видеорежима рабочего стола и в Windows NT, и в Windows 95. Когда вы приказываете Windows сменить видеорежим, новый режим активизируется на 15 секунд, после чего восстанавливается предыдущий. Затем диалоговое окно спрашивает, правильно ли система работала с новыми параметрами.

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


Зачем нужна многопоточность?


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

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

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



Захват клавиатуры


Итак, мы инициализировали DirectInput и подготовили клавиатуру к работе; теперь необходимо захватить ее. Для этой цели используется функция OnActivate(), потому что клавиатуру приходится захватывать при каждой активизации нашего приложения. Функция OnActivate() выглядит так:


void QwertyWin::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { DirectDrawWin::OnActivate(nState, pWndOther, bMinimized);

if (nState!=WA_INACTIVE && keyboard) { TRACE("keyboard->Acquire()\n"); keyboard->Acquire(); } }


После вызова версии OnActivate() базового класса мы проверяем, происходит ли активизация приложения (функция OnActivate() вызывается и в случае деактивизации, когда активным становится другое приложение). Если проверка дает положительный результат, мы вызываем функцию Acquire() интерфейса DirectInputDevice.

Перед вызовом Acquire() можно проверить, не была ли клавиатура захвачена ранее, но в этом нет необходимости. DirectInput игнорирует лишние вызовы функции Acquire().



Захват мыши и клавиатуры


Перед тем как получать данные от клавиатуры или мыши, необходимо предварительно захватить их. Кроме того, устройство приходится захватывать заново, если приложение некоторое время было неактивным. И исходный, и все последующие захваты устройства выполняются в обработчике OnActivate():


void SmearWin::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized) { DirectDrawWin::OnActivate(nState, pWndOther, bMinimized); if (nState!=WA_INACTIVE) { if (keyboard) { TRACE("keyboard->Acquire()\n"); keyboard->Acquire(); } if (mouse) { TRACE("mouse->Acquire()\n"); mouse->Acquire(); } } }


Функция Acquire() вызывается для каждого устройства независимо от того, уступалось ли оно. DirectInput игнорирует лишние вызовы Acquire().



Захват устройств


Приложение DirectDraw в случае необходимости может уступить видеопамять другому приложению и восстановить ее, когда исходное приложение снова получит фокус. В DirectInput приложение тоже может потерять устройство и восстановить контроль над ним перед тем, как продолжить работу. В таких случаях говорят, что приложение захватывает устройство (acquire) или уступает его (unacquire). Для получения данных необходимо захватить устройство. Приложение может уступить устройство по требованию (доступ к устройству передается Windows или другому приложению) или автоматически (например, если DirectInput отбирает право доступа к устройству, чтобы передать его другому приложению).

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



Наш интенсивный краткий курс DirectDraw


Наш интенсивный краткий курс DirectDraw подходит к концу. В главе 2 мы поговорим о проектировании и оптимизации приложений, а также о том, что ждет DirectDraw в ближайшем будущем.

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


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

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


В этой главе мы изучили почти весь код, сгенерированный AppWizard. Рассмотренное нами базовое приложение нетрудно изменить, поэтому попробуйте немного поэкспериментировать. Например, попытайтесь добавить в программу Bounce дополнительные поверхности или замените вызовы BltSurface() на BltFast() и посмотрите, что получится.

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

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


В этой главе мы рассмотрели две демонстрационные программы и воспользовались такими возможностями DirectDraw, как переключение видеорежимов и частот смены кадров, а также применили цветовые ключи. Для переключения видеорежимов и частот использовалась функция EnumDisplayModes() интерфейса DirectDraw в сочетании с функцией SetDisplayMode(), а для работы с цветовыми ключами — функции SetColorKey() и BltFast() интерфейса DirectDrawSurface. Вывод текста в программах осуществлялся с помощью функции GetDC() интерфейса DirectDrawSurface и текстовых функций Win32.

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

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


В этой главе я упоминал о том, что загрузить растровое изображение на поверхность можно и другим, более простым способом. Вспомните — в интерфейс DirectDrawSurface входит функция GetDC(), которая позволяет работать с поверхностями с помощью обычных функций Win32. Реализующая этот подход функция может выглядеть так:

BOOL CopyBmp (LPDIRECTDRAWSURFACE surface, HBITMAP bmp, int x, int y) { if (bmp==0) { TRACE("no bmp specified\n"); return FALSE; } if (surface==0) { TRACE("no surface specified\n"); return FALSE; } HDC imageDC = CreateCompatibleDC( 0 ); SelectObject( imageDC, bmp);
BITMAP bitmap; GetObject( bmp, sizeof(bitmap), &bitmap); int w=bitmap.bmWidth; int h=bitmap.bnHeight;
DDSURFACEDESC desc; desc.dwSize = sizeof(desc); desc.dwFlags = DDSD+HEIGHT |DDSC_WIDTH; surface->GetSurfaceDesc( &desc); HDC dc; HRESULT result; if ((result=surface->GetDC( &dc ))==DD_OK)) { Stretchblt( dc, 0, 0, desc.dwWidth, desc.dwHeight, imageDC, x, y, w, h, SRCCOPY ); surface->ReleaseDC( dc ); } DeleteDC (imageDC );
return result==DD_OK; }

Эта функция не имеет никакого отношения к программе этой главы, ее даже нет на CD-ROM. Она приведена с единственной целью — показать, что с помощью функции GetDC() интерфейса DirectDrawSurface и функции Win32 наподобие StretchBlt() можно легко скопировать растровое изображение Windows на поверхность. Разумеется, в этом случае приходится жертвовать скоростью, поскольку механизм GDI не отличается особым быстродействием, а его функции не поддаются оптимизации.

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

В
главе 6 мы узнаем, как наделить приложение поддержкой DirectInput. Обходя традиционные механизмы ввода Windows, DirectInput позволяет с максимальной эффективностью получать данные от таких устройств, как клавиатура или мышь.

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru

В этой главе мы узнали, как организовать в своих приложениях поддержку DirectInput и обойти традиционные механизмы ввода Windows. Такое решение повышает быстродействие и дает больше возможностей для обработки пользовательского ввода. По этим причинам DirectInput будет использоваться во всех оставшихся программах этой книги.

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

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


Вывод курсора в DirectDraw — одна из тех досадных проблем, которые часто возникают перед разработчиками. Однако частичное обновление экрана и многопоточность пригодятся вам и в других ситуациях.

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


Наше знакомство с воспроизведением видеороликов подходит к концу. Честно говоря, чтобы превратить программу AviPlay в полноценный проигрыватель AVI-файлов, вам придется еще немало потрудиться. Необходимо организовать поддержку звука и хронометраж, не говоря уже о том, что VFW обладает многими странностями и в работе с ним приходится много экспериментировать.

И последнее замечание. По неизвестным мне причинам VFW отказывается работать с AVI-файлами, сжатыми кодеками IR32 и IR42 (возможно, есть и другие, но я заметил эти два). С другой стороны, AVI-файлы, использующие кодеки MS-CRAM и Cinepak, работают нормально.

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

Следующая глава

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru


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

Эта глава была последней - мы рассмотрели все программы. Тем не менее остались некоторые интересные темы, которые не обсуждались в книге. Мы поговорим о них в приложении А.

Приложение

С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru

Замечания о Visual C++


Как бы вы ни относились к Visual C++, приходится признать: это чрезвычайно мощный пакет. В нем объединены компилятор, компоновщик, отладчик, профайлер и редактор ресурсов — я перечислил лишь основные компоненты. С каждой новой версией он становится все больше и мощнее. Однако сказанное относилось только к C++! Сам по себе Visual C++ является интегрированным компонентом Developer Studio — многоцелевой и многоязыковой платформы разработчика, с которой мне никогда не надоедает работать.

Visual C++ тоже не стоит на месте. Хорошо это или плохо, но в него постоянно вносятся изменения, а значит — возникают новые проблемы. К тому же Microsoft иногда стремится опередить события и под давлением маркетинговых и финансовых соображений выпускает продукты, которым полагалось бы находиться на стадии бета-тестирования. Трудно сказать, относится ли сказанное к Visual C++ 5.0. Впрочем, на момент написания книги уже существовало дополнение (service pack), исправляющее целый ряд багов. Чаще всего от ошибок страдают новые средства Visual C++ (например, говорят, что их хватает в новой библиотеке ActiveX Template Library (ATL)). К счастью, в данной книге эти новые возможности не используются.

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

С другой стороны, в Visual C++ появилось несколько хороших, хотя и запоздалых возможностей. Один из примеров — новые ключевые слова bool, true и false. В течение некоторого времени они уже входили в стандарт C++; хорошо, что мы наконец сможем ими пользоваться.

В этом разделе мы поговорим о Visual C++. Одни вопросы относятся к программам из этой книги, другие — ко всем проектам, написанным на Visual C++.



Завершение


Как бы ни была хороша программа Bounce, рано или поздно вам захочется убрать ее с экрана. Нажатие клавиши Escape завершает работу программы. Это происходит в обработчике OnKeyDown():


void bounceWin::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { if (nChar==VK_ESCAPE) PostMessage( WM_CLOSE ); DirectDrawWin::OnKeyDown(nChar, nRepCnt, nFlags); }


Приложение завершает работу, отправляя сообщение WM_CLOSE. В нашем приложении на это сообщение реагирует и класс окна, и класс приложения. Класс окна отвечает сообщением WM_DESTROY, для которого в DirectDrawWin предусмотрен обработчик OnDestroy(). Класс DirectDrawWin в данном обработчике освобождает объекты DirectDraw и всю память, занимаемую приложением. Функция OnDestroy() выглядит так:


void DirectDrawWin::OnDestroy() { if (primsurf) primsurf->Release(), primsurf=0;

if (palette) palette->Release(), palette=0;

if (ddraw2) ddraw2->Release(), ddraw2=0;

for (int i=0;i<totaldrivers;i++) { if (driver[i].guid) delete [] driver[i].guid; free( driver[i].desc ); free( driver[i].name ); } }


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

Класс приложения обрабатывает завершение в функции ExitInstance(), в которой удаляется класс окна:


int DirectDrawApp::ExitInstance() { delete ddwin;

return CWinApp::ExitInstance(); }


На этом наше знакомство с программой Bounce заканчивается. Однако до сих пор речь шла только о полноэкранных приложениях. Оставшаяся часть этой главы посвящена оконным приложениям.



Завершение приложения


Завершить работу DirectInput несложно — для этого достаточно освободить все интерфейсы DirectInput. В нашей программе это происходит в функции OnDestroy():


void QwertyWin::OnDestroy() { DirectDrawWin::OnDestroy();

if (dinput) dinput->Release(), dinput=0; if (keyboard) { keyboard->Unacquire(); keyboard->Release(), keyboard=0; } }



Перед завершением приложения MFC вызывает функцию OnDestroy(); мы воспользуемся ею для освобождения объектов DirectInput. Функция OnDestroy() выглядит так:


void SmearWin::OnDestroy() { DirectDrawWin::OnDestroy(); if (dinput) dinput->Release(), dinput=0; if (keyboard) { keyboard->Unacquire(); keyboard->Release(), keyboard=0; }

if (mouse) { mouse->Unacquire(); mouse->Release(), mouse=0; } }


Функция OnDestroy() просто освобождает каждый объект DirectInput (и вызывает одноименную функцию базового класса).




Осталось лишь поговорить о том, как завершается работа приложения. Эта тема неоднократно рассматривалась, и ее можно было бы пропустить, но для программы Cursor она важна из-за наличия дополнительного потока. Мы должны не только послать потоку ввода сигнал о завершении, но и проследить за тем, чтобы поток завершился до уничтожения объекта устройства мыши и поверхностей DirectDraw. В противном случае он может попытаться обратиться к мыши или обновить первичную поверхность после того, как соответствующие объекты перестанут существовать. Функция OnDestroy() выглядит так:


void CursorWin::OnDestroy() { critsection.Lock();

DirectDrawWin::OnDestroy();

if (mouse) { TRACE("mouse->Unacquire()\n"); mouse->Unacquire(); TRACE("sending mouse quit message...\n"); mouse_event[quit_event_index]->SetEvent();

Sleep(100); // дать потоку мыши возможность ответить TRACE("Releasing mouse pointer...\n"); mouse->Release(), mouse=0;

delete mouse_event[mouse_event_index]; delete mouse_event[quit_event_index]; }

if (keyboard) keyboard->Release(), keyboard=0;

if (dinput) dinput->Release(), dinput=0;

critsection.Unlock(); }


Когда MFC вызывает функцию OnDestroy(), основной поток заведомо не обновляет экран, потому что он занят выполнением этой функции. Тем не менее мы не знаем, не обновляется ли экран потоком ввода. Чтобы поток ввода закончил последнее обновление, мы блокируем критическую секцию.

Далее мы уступаем мышь. Устройство перестает генерировать новые события, которые заставили бы поток ввода попытаться снова обновить экран. Затем функция CEvent::SetEvent() посылает потоку ввода сигнал о завершении.

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