• Поверхности
  • BMP-файлы
  • Программа BmpView 
  • Глава 5. Поверхности и форматы пикселей     

    Наверное, это самая важная глава во всей книге. Она посвящена поверхностям, а поверхности — главное, для чего создавалась библиотека DirectDraw. Поверхности DirectDraw позволяют хранить изображения, копировать и изменять их, переключать кадры и выводить графическую информацию на экран. Все интерфейсы DirectDraw в первую очередь ориентированы на работу с поверхностями. Интерфейс DirectDrawPalette облегчает интерпретацию палитровых поверхностей; интерфейс DirectDrawClipper определяет, какая часть (или части) поверхности будут копироваться при блиттинге; наконец, сам интерфейс DirectDraw обеспечивает основные средства для работы с поверхностями.

    Поверхности


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

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

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

    Несмотря на все преимущества прямого линейного доступа, при манипуляциях с поверхностями программист должен соблюдать осторожность. Например, чтобы получить указатель на память поверхности, ее необходимо предварительно заблокировать. Чаще всего такая блокировка заставляет DirectDraw временно отключать основные механизмы Windows. Если вы забудете разблокировать поверхность или ваша программа «зависнет» при заблокированной поверхности, скорее всего, придется перезагружать компьютер. Кроме того, для проверки правильности работы кода между вызовами Lock() и Unlock() нельзя пользоваться отладчиками.

    СОВЕТ

    Новые возможности DirectX 5

    DirectX 5 позволяет указать DirectDraw, что во время блокировки поверхностей можно обойтись без остановки механизмов Windows. DirectDraw постарается заблокировать поверхность, но при этом обойтись без обычных проблем.

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

    Для прямого доступа к поверхности нужно знать формат ее пикселей. Этот формат определяет способ хранения цветовых данных каждого пикселя. Он может изменяться в зависимости от видеоустройства и даже от видеорежима. Форматы пикселей особенно сильно различаются для поверхностей High Color (16-битных).

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

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

    Глубина пикселей 

    Глубина пикселей показывает, сколько разных цветов может быть представлено одним пикселем поверхности. Глубина пикселей также влияет на объем памяти, необходимой для представления поверхности. В DirectDraw предусмотрена поддержка четырех глубин пикселей: 8-битных (палитровых), 16-битных (High Color), 24-битных и 32-битных (объединяемых термином True Color).

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

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

    Пиксели High Color (16-битные) выглядят несколько сложнее, однако результат часто оправдывает усилия. Простота использования, характерная для беспалитровых поверхностей, сочетается в них с умеренным расходом памяти (по сравнению с пикселями глубины True Color). Каждый пиксель High Color содержит не индекс, а цвет. Цвета выражаются в виде комбинации трех цветовых составляющих: красной, зеленой и синей (RGB).

    Пиксели True Color делятся на две категории (24- и 32-битные), но в обоих случаях используются только 24 бита данных RGB. Лишние 8 бит 32-битных пикселей иногда используются для хранения альфа-канала (то есть данных о прозрачности пикселя). К сожалению, в DirectDraw все еще отсутствует возможность эмуляции альфа-наложения, так что лишние биты 32-битного пикселя часто пропадают впустую.

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

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

    Рис. 5.1. Зависимость требований к памяти от глубины пикселей


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

    Для примера возьмем 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-битные поверхности


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

    Форматы пикселей 

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

    В 16-битных пикселях хранится непосредственно цветовая информация. 16-битные форматы пикселей делятся на две основные разновидности: в первой каждая RGB-составляющая представлена 5 битами (оставшийся бит не используется), а во второй 5 битами представлены только красная и синяя составляющие, а для зеленой используется 6 бит. Для этих форматов часто применяются условные обозначения 5-5-5 и 5-6-5 соответственно.

    В формате 5-5-5 каждая цветовая составляющая может принимать значения из интервала от 0 до 31. Чем больше значение составляющей, тем интенсивнее она проявляется в результирующем цвете. Формат 5-6-5 работает аналогично, за исключением того, что зеленая составляющая может принимать значения из интервала 0–63. Эти два вида 16-битных пикселей изображены на рис. 5.5.

    Дело осложняется тем, что форматы 5-5-5 и 5-6-5 тоже делятся на два типа. На рис. 5.5 изображен RGB -формат, в котором цветовые составляющие хранятся в порядке «красный, зеленый, синий». Также существует BGR-формат, в котором красная и синяя составляющая меняются местами. Вряд ли в ближайшем будущем появятся еще какие-нибудь варианты.

    Рис. 5.5. Два распространенных 16-битных формата пикселей


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

    С пикселями формата True Color работать проще, потому что каждая из RGB-составляющих представлена одним байтом. В этом случае значение каждой составляющей принадлежит интервалу 0–255; ноль означает, что составляющая вообще не влияет на результирующий цвет, а 255 - что ее влияние максимально. Форматы пикселей для 24- и 32-битных вариантов изображены на рис. 5.6.

    24- и 32-битные пиксели, как и 16-битные, делятся на две разновидности: RGB и BGR. Следовательно, код для работы с пикселями True Color должен использовать сведения о формате, полученные от DirectDraw, и не делать никаких безусловных предположений.

    Напоминаю, что альфа-байт в 32-битных пикселях часто остается неиспользованным. Если ваша программа не работает с альфа-данными, вы можете выбирать между 24- и 32-битным видеорежимами. Работа в 24-битном режиме экономит память.

    Рис. 5.6. Типичные форматы пикселей True Color


    Получение данных о формате пикселей  

    Сведения о формате пикселей поверхности можно получить функцией GetPixelFormat() интерфейса DirectDrawSurface, в которой для передачи данных используется структура DDPIXELFORMAT. Функция GetPixelFormat() применяется так:

    DDPIXELFORMAT format;

    ZeroMemory(&format, sizeof(format));

    format.dwSize=sizeof(format);

    surf->GetPixelFormat(&format);

    Структура DDPIXELFORMAT содержит четыре поля, представляющих для нас интерес:

    • dwRGBBitCount

    • dwRBitMask

    • dwGBitMask

    • dwBBitMask

    Поле dwRGBBitCount показывает глубину пикселей поверхности. Три оставшихся поля являются масками, определяющими, в каких битах пикселя хранятся данные красной, зеленой и синей составляющих. Например, типичные значения полей для поверхности High Color формата 5-6-5 приведены в табл. 5.1.

    Три маски показывают, в каких позициях пикселя хранятся данные отдельных цветовых составляющих. С помощью этих масок можно корректно читать и записывать данные пикселя независимо от его формата.


     Таблица 5.1. Типичные данные формата для 16-битных пикселей

    Поле Значение Двоичное значение
    dwRGBBitCount 16 (неважно)
    dwRBitMask 63488 1111100000000000
    dwGBitMask 2016 0000011111100000
    dwBBitMask 31 0000000000011111

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

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


    Таблица 5.2. Типичные данные формата для 16-битных пикселей

    Поле Значение Двоичное значение Стартовый бит Количество бит
    dwRBitMask 63488 1111100000000000 11 5
    dwGBitMask 2016 0000011111100000 5 6
    dwBBitMask 31 0000000000011111 0 5

    Однако до сих пор мы рассматривали лишь 16-битные пиксели. 8-битные пиксели нас не интересуют, но перед тем, как идти дальше, необходимо уделить внимание пикселям формата True Color. В табл. 5.3 приведены данные формата пикселей (в том числе две вычисленные величины для каждой цветовой составляющей) для типичного 24-битного формата.


    Таблица 5.3. Типичные данные формата для 24-битных пикселей

    Поле Значение Двоичное значение Стартовый бит Количество бит
    dwRBitMask 16711680 111111110000000000000000 16 8
    dwGBitMask 65280 000000001111111110000000 8 8
    dwBBitMask 255 000000000000000011111111 0 8

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

    Переменные формата пикселей в классе DirectDrawWin

    При описании класса DirectDrawWin в главе 3 мы видели, что функция DirectDrawWin::ActivateDisplayMode() после активизации нового видеорежима, но до создания вспомогательных поверхностей приложения, вызывает функцию StorePixelFormatData(). Описание этой функции было отложено до настоящего момента.

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

    BOOL DirectDrawWin::StorePixelFormatData() {

     DDPIXELFORMAT format;

     ZeroMemory(&format, sizeof(format));

     format.dwSize=sizeof(format);

     if (primsurf->GetPixelFormat(&format)!=DD_OK) {

      TRACE("StorePixelFormatData() failed\n");

      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;

    }

    Функция StorePixelFormatData() присваивает значения шести переменным формата с помощью масок, полученных функцией GetPixelFormat() интерфейса DirectDrawSurface. Это следующие переменные:

    • loREDbit

    • numREDbits

    • loGREENbit

    • numGREENbits

    • loBLUEbit

    • numBLUEbits

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

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

    Для прямого доступа к поверхности необходимо предварительно вызвать функцию Lock() интерфейса DirectDrawSurface. Lock() получает экземпляр структуры DDSURFACEDESC и возвращает указатель на левый верхний пиксель поверхности, шаг поверхности, ее размеры и даже формат пикселей (структура DDSURFACEDESC содержит экземпляр DDPIXELFORMAT, поэтому вызов GetPixelFormat() интерфейса DirectDrawSurface оказывается необязательным). Прототип функции Lock() выглядит так:

    HRESULT Lock(LPRECT rect, LPDDSURFACEDESC desc, DWORD flags, HANDLE event);

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

    Второй аргумент функции Lock() — структура DDSURFACEDESC, которая используется для возвращения указателя на память поверхности (поле lpSurface) и шага поверхности (поле lPitch). Функция Lock() (как и другие функции DirectDraw) требует правильно присвоить значение полю dwSize структуры DDSURFACEDESC.

    Третий аргумент используется для настройки параметров Lock(). В него могут входить следующие флаги:

    • DDLOCK_EVENT

    • DDLOCK_READONLY

    • DDLOCK_WRITEONLY

    • DDLOCK_SURFACEMEMORYPTR

    • DDLOCK_WAIT

    На момент выхода DirectX 5 флаг DDLOCK_EVENT не поддерживался. Возможно, в будущих версиях DirectDraw он будет использоваться совместно с последним аргументом Lock() для реализации альтернативного метода блокировки поверхностей.

    Флаги DDLOCK_READONLY и DDLOCK_WRITEONLY следует использовать в том случае, когда доступ к памяти поверхности осуществляется исключительно для чтения или записи. В большинстве ситуаций эти флаги ни на что не действуют, однако в видеорежимах «Mode X» DirectDraw использует их для оптимизации доступа к поверхности.

    Флаг DDLOCK_SURFACEMEMORYPTR необязателен, потому что он задает поведение Lock(), которое и так является стандартным. Lock() возвращает указатель на память поверхности как с этим флагом, так и без него, поэтому мы не станем использовать его в своих программах (флаг DDLOCK_SURFACEMEMORYPTR на самом деле определен равным 0, так что я нисколько не преувеличиваю, говоря, что он ни на что не влияет).

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

    BMP-файлы

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

    Формат BMP-файлов

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

    BMP-файлы состоят из трех основных частей:

    • заголовок;

    • палитра;

    • графические данные (значения пикселей).

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

    Палитра присутствует только в BMP-файлах, содержащих палитровые изображения (с глубиной пикселей 8 бит и менее). К 8-битным изображениям прикладывается палитра, состоящая из не более чем 256 элементов.

    Графические данные — это и есть само изображение. Формат этих данных зависит от глубины пикселей. Хотя BMP-файлы делятся на несколько типов, мы ограничимся 8-битными и 24-битными изображениями. 8-битные BMP-файлы будут использоваться для работы с 8-битными поверхностями, а 24-битные — для беспалитровых поверхностей. Хотя, по слухам, в природе существуют 16-битные и 32-битные BMP-файлы, они встречаются очень редко — например, мне таковые ни разу не попадались. Впрочем, это не имеет особого значения, так как 24-битную графику можно легко преобразовать в 16- или 32-битный формат.

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

    Данные заголовка 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 не представляют для нас интереса, поэтому я не стану утомлять вас их обсуждением. 

    Палитра 

    Палитра в BMP-файлах хранится в виде списка структур RGBQUAD, где каждый элемент представляет отдельный цвет. Структура RGBQUAD объявляется так:

    typedef struct tagRGBQUAD {

     BYTE rgbBlue;

     BYTE rgbGreen;

     BYTE rgbRed;

     BYTE rgbReserved;

    } RGBQUAD;

    В первых трех полях хранятся цветовые RGB-составляющие. На поле rgbReserved мы не будем обращать внимания (предполагается, что оно равно нулю). Как я упоминал выше, количество структур RGBQUAD в BMP-файле определяется полем biClrUsed. Тем не менее обычно 8-битные BMP-файлы содержат 256 структур RGBQUAD. В 24-битных RGB-файлах структуры RGBQUAD отсутствуют. 

    Графические данные 

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

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

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

    Организация доступа к поверхностям 

    В наших программах чтением BMP-файлов занимается класс DirectDrawWin. Впервые эта возможность была использована в главе 3, где в программе Bounce BMP-файл загружался на поверхность. То же самое происходит и в программе BmpView, но сначала давайте рассмотрим соответствующий программный код.

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

    Функция CreateSurface() 

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

    Давайте посмотрим, как реализована функция CreateSurface() (см. листинг 5.1).


    Листинг 5.1. Функция CreateSurface()

    LPDIRECTDRAWSURFACE DirectDrawWin::CreateSurface(LPCTSTR filename, BOOL installpalette) {

     int imagew, imageh;

     GetBmpDimensions(filename, imagew, imageh);

     LPDIRECTDRAWSURFACE surf=CreateSurface(imagew, imageh);

     if (surf==0) {

      TRACE("CreateSurface(filename) failed to create surface\n");

      return 0;

     }

     ifstream bmp(filename, ios::binary | ios::nocreate);

     if (!bmp.is_open()) {

      TRACE("LoadSurface: cannot open Bmp file\n");

      return 0;

     }

     BITMAPFILEHEADER bmpfilehdr;

     bmp.read((char*)&bmpfilehdr, sizeof(bmpfilehdr));

     char* ptr=(char*)&bmpfilehdr.bfType;

     if (*ptr!='B' || *++ptr!='M') {

      TRACE("invalid bitmap\n");

      return 0;

     }

     BITMAPINFOHEADER bmpinfohdr;

     bmp.read((char*)&bmpinfohdr, sizeof(bmpinfohdr));

     bmp.seekg(sizeof(bmpfilehdr)+bmpinfohdr.biSize, ios::beg);

     int imagebitdepth=bmpinfohdr.biBitCount;

     int imagesize=bmpinfohdr.biSizeImage;

     if (imagesize==0) imagesize=((imagew*(imagebitdepth/8)+3) & ~3)*imageh;

     if (bmpinfohdr.biCompression!=BI_RGB) {

      TRACE("compressed BMP format\n");

      return 0;

     }

     TRACE("loading '%s': width=%d height=%d depth=%d\n", filename, imagew, imageh, imagebitdepth);

     if (imagebitdepth==8) {

      int ncolors;

      if (bmpinfohdr.biClrUsed==0)   ncolors=256;

      else   ncolors=bmpinfohdr.biClrUsed;

      RGBQUAD* quad=new RGBQUAD[ncolors];

      bmp.read((char*)quad, sizeof(RGBQUAD)*ncolors);

      if (installpalette) CreatePalette(quad, ncolors);

      delete[] quad;

     }

     BYTE* buf=new BYTE[imagesize];

     bmp.read(buf, imagesize);

     if (!Copy_Bmp_Surface(surf, &bmpinfohdr, buf)) {

      TRACE("copy failed\n");

      delete[] buf;

      surf->Release();

      return 0;

     }

     delete[] buf;

     return surf;

    }

    Сначала эта функция определяет размеры изображения из BMP-файла с помощью функции GetBmpDimensions() — простой функции класса DirectDrawWin, которая открывает BMP-файл и извлекает из заголовка ширину и высоту изображения. На основании полученных данных создается новая поверхность с использованием версии CreateSurface(), которая создает поверхность по ее размерам. Новая поверхность заполнена случайными пикселями, но мы не стираем ее, потому что вскоре значение каждого пикселя будет задано в соответствии с содержимым BMP-файла.

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

    Дополнительные данные заголовка извлекаются с помощью структуры BITMAPINFOHEADER. Обратите внимание: после заполнения структуры текущая позиция в файле ifstream изменяется в соответствии со значением поля biSize. Это сделано для того, чтобы в будущем, при увеличении размера структуры BITMAPINFOHEADER, наша программа нормально работала с новыми BMP-файлами.

    Ширина и высота изображения уже известны, поэтому читать значения полей biWidth и biHeight структуры BITMAPINFOHEADER не нужно. Функция CreateSurface() считывает глубину пикселей (biBitCount) и размер изображения (biSizeImage). Как упоминалось выше, поле biSizeImage часто равно нулю, поэтому мы проверяем его значение. Снова приведу соответствующий фрагмент кода:

    int imagesize=bmpinfohdr.biSizeImage;

    if (imagesize==0) imagesize=((imagew*(imagebitdepth/8)+3) & ~3)*imageh;

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

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

    Если изображение является палитровым, мы загружаем палитру. Количество элементов палитры в файле определяется полем biClrUsed, но это поле также может быть равно нулю. В этом случае предполагается, что присутствуют все 256 элементов палитры. Палитра загружается лишь в том случае, если параметр installpalette равен TRUE; тогда вызывается функция CreatePalette(). Вскоре мы рассмотрим код этой функции.

    Следующий этап — чтение графических данных, которое в нашем случае выполняется одним вызовом функции ifstream::read(). Графические данные передаются функции Copy_Bmp_Surface(), отвечающей за пересылку данных новой поверхности. После возврата из функции Copy_Bmp_Surface() буфер с графическими данными освобождается. BMP-файл автоматически закрывается при возвращении из функции CreateSurface() (поскольку локальный объект ifstream выходит из области видимости). 

    Функция CreatePalette() 

    Если второй аргумент функции CreateSurface() равен TRUE, CreatePalette() создает и заполняет объект DirectDrawPalette данными, полученными из BMP-файла. Функция CreatePalette() выглядит так:

    BOOL DirectDrawWin::CreatePalette(RGBQUAD* quad, int ncolors){

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

     PALETTEENTRY pe[256];

     ZeroMemory(pe, sizeof(pe));

     for(int i=0; i<ncolors; i++) {

      pe[i].peRed = quad[i].rgbRed;

      pe[i].peGreen = quad[i].rgbGreen;

      pe[i].peBlue = quad[i].rgbBlue;

     }

     HRESULT r=ddraw2->CreatePalette(DDPCAPS_8BIT | DDPCAPS_ALLOW256, pe, &palette, 0);

     if (r!=DD_OK) {

      TRACE("failed to reate DirectDraw palette\n");

      return FALSE;

     }

     primsurf->SetPalette(palette);

     return TRUE;

    }

    Палитры DirectDraw создаются функцией CreatePalette() интерфейса DirectDraw, которой передается массив структур PALETTEENTRY. Чтобы выполнить это требование, приходится преобразовывать массив структур RGBQUAD, извлеченный из BMP-файла, во временный массив (структуры PALETTEENTRY и RGBQUAD очень похожи, поэтому такое преобразование оказывается тривиальным). Затем созданный массив передается функции CreatePalette(). Флаг DDPCAPS_ALLOW256 сообщает, что мы намерены задать значения всех 256 элементов палитры. Если вы пропустили главу 4 (конечно же, нет!), вернитесь к ней и ознакомьтесь с возможными аспектами использования этого флага.

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

    Передача графических данных 

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

    BOOL DirectDrawWin::Copy_Bmp_Surface(LPDIRECTDRAWSURFACE surf, BITMAPINFOHEADER* bmphdr, BYTE* buf) {

     if (surf==0 || bmphdr==0 || buf==0) return FALSE;

     int imagew=bmphdr->biWidth;

     int imageh=bmphdr->biHeight;

     int imagebitdepth=bmphdr->biBitCount;

     BOOL ret=FALSE;

     if (imagebitdepth==8) {

      if (displaydepth==8) ret=Copy_Bmp08_Surface08(surf, buf, imagew, imageh);

     } else if (imagebitdepth==24) {

      if (displaydepth==16) ret=Copy_Bmp24_Surface16(surf, buf, imagew, imageh);

      else if (displaydepth==24) ret=Copy_Bmp24_Surface24(surf, buf, imagew, imageh);

       else if (displaydepth==32) ret=Copy_Bmp24_Surface32(surf, buf, imagew, imageh);

     }

     return ret;

    }

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

    8-битные поверхности

    Начнем с самой простой из четырех функций, Copy_Bmp08_Surface08(). Она выглядит так:

    BOOL DirectDrawWin::Copy_Bmp08_Surface08(LPDIRECTDRAWSURFACE surf,      BYTE* bmpbuf, int w, int h) {

     if (surf==0 || bmpbuf==0) return FALSE;

     DDSURFACEDESC desc;

     ZeroMemory(&desc, sizeof(desc));

     desc.dwSize = sizeof(desc);

     HRESULT r=surf->Lock(0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0);

     if (r!=DD_OK) {

      TRACE("ShowBmp: Lock() failed\n");

      return FALSE;

     }

     int bytesgiven=(w+3) & ~3;

     BYTE* surfbits = (BYTE*)desc.lpSurface;

     BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

     for(int i=0; i<h; i++) {

      memcpy(surfbits, imagebits, w);

      surfbits += desc.lPitch;

      imagebits -= bytesgiven;

     }

     surf->Unlock(0);

     return TRUE;

    }

    После проверки обоих аргументов-указателей мы подготавливаем экземпляр структуры DDSURFACEDESC(desc) и используем его при вызове функции Lock() интерфейса DirectDrawSurface. После возвращения из функции Lock() поле lpSurface содержит указатель на память поверхности, и мы можем спокойно изменять содержимое поверхности через этот указатель до вызова Unlock(). Безопасная работа с поверхностью стала возможной только потому, что мы указали флаг DDLOCK_WRITEONLY. Если вы собираетесь осуществлять и чтение, и запись, не устанавливайте этот флаг.

    Далее мы инициализируем целую переменную bytesgiven. Присваиваемое значение определяется шириной изображения (w), выровненного по границе параграфа. Получившаяся величина равна объему памяти, необходимой для хранения одной строки пикселей. Если ширина изображения кратна четырем, переменная bytesgiven совпадает с w.

    Указатель на поверхность (surfbits) инициализируется значением поля lpSurface. Этот указатель используется для обращений к памяти поверхности. Указатель на графические данные (imagebits) инициализируется адресом последней строки пикселей BMP-файла, поскольку в формате BMP изображение хранится в перевернутом виде.

    Затем мы в цикле перебираем все строки пикселей изображения. Благодаря тому, что формат графических данных BMP-файла совпадает с форматом поверхности, для копирования можно воспользоваться функцией memcopy(). Для поверхностей остальных типов такая удобная возможность часто отсутствует. Поле lPitch определяет смещение для указателя на поверхность при переходе к следующей строке. Вспомните, что в этом поле хранится шаг поверхности, который может не совпадать с ее шириной. Целая переменная bytesgiven аналогичным образом используется для перехода к следующей строке буфера графических данных. Поскольку чтение начинается с конца буфера, указатель imagebits уменьшается с каждой очередной итерацией.

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

    16-битные поверхности

    Загрузка 8-битных изображений выполняется достаточно просто. Давайте перейдем к 16-битным поверхностям, с ними дело обстоит значительно сложнее. Помимо учета разных типов 16-битных форматов пикселей нам придется сокращать количество цветов. 24-битные данные передаются на 16-битную поверхность, поэтому во время передачи необходимо «урезать» каждую цветовую составляющую. Функция Copy_Bmp24_Surface16() приведена в листинге 5.2.


    Листинг 5.2. Функция Copy_Bmp24_Surface16()

    BOOL DirectDrawWin::Copy_Bmp24_Surface16(LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h) {

     if (surf==0 || bmpbuf==0)  return FALSE;

     DDSURFACEDESC desc;

     ZeroMemory(&desc, sizeof(desc));

     desc.dwSize = sizeof(desc);

     HRESULT r=surf->Lock(0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0);

     if (r!=DD_OK) {

      TRACE("Copy_Bmp24_Surface16: Lock() failed\n");

      return FALSE;

     }

     int bytesrequired=w*3;

     int bytesgiven=(bytesrequired+3) & ~3;

     BYTE* surfbits = (BYTE*)desc.lpSurface;

     BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

     float REDdiv=(float)256/(float)pow(2, numREDbits);

     float GREENdiv=(float)256/(float)pow(2, numGREENbits);

     float BLUEdiv=(float)256/(float)pow(2, numBLUEbits);

     for(int i=0; i<h; i++) {

      USHORT* pixptr=(unsigned short*)surfbits;

      RGBTRIPLE* triple=(RGBTRIPLE*)imagebits;

      for (int p=0;p>w;p++) {

       float rf=(float)triple->rgbtRed/REDdiv;

       float gf=(float)triple->rgbtGreen/GREENdiv;

       float bf=(float)triple->rgbtBlue/BLUEdiv;

       WORD r=(WORD)((WORD)rf<<loREDbit);

       WORD g=(WORD)((WORD)gf<<loGREENbit);

       WORD b=(WORD)((WORD)bf<<loBLUEbit);

       *pixptr = (WORD)(r|g|b);

       triple++;

       pixptr++;

      }

      surfbits += desc.lPitch;

      imagebits –= bytesgiven;

     }

     surf->Unlock(0);

     return TRUE;

    }

    Хотя по своей структуре функция Copy_Bmp24_Surface16() напоминает Copy_Bmp 08_Surface08(), она устроена сложнее по причинам, уже упоминавшимся, а также потому, что значение каждого пикселя приходится задавать отдельно. Давайте посмотрим, что происходит в этой функции.

    Сначала функция Lock() интерфейса DirectDrawSurface используется для получения указателя на поверхность. Затем мы инициализируем две целые переменные, bytesrequired и bytesgiven. Значение bytesrequired равно количеству байт, необходимых для представления строки пикселей. Поскольку мы работаем с 24-битными пикселями, для получения этой величины достаточно умножить ширину изображения на три (по три байта на пиксель). По значению bytesrequired рассчитывается значение bytesgiven, которое равно количеству байт для хранения строки пикселей в памяти (с учетом выравнивания по границе параграфа). Значение bytesgiven используется для перебора строк пикселей в графических данных BMP-файла.

    Затем мы инициализируем указатели surfbits и imagebits; первый указывает на память поверхности, а второй — на буфер графических данных. Как и в функции Copy_Bmp08_Surface08(), imagebits указывает на последнюю строку буфера.

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

    Назначение пикселей происходит во вложенном цикле. Внешний цикл перебирает строки пикселей, а внутренний задает значение для каждого пикселя строки. Внутренний цикл инициализирует два указателя, pixptr и triple, которые используются для обращения к текущему пикселю. Переменная pixptr указывает на память поверхности, а triple - на буфер графических данных. Обратите внимание — pixptr объявлен как указатель на 16-битный тип USHORT. В этом случае для перехода к следующему пикселю достаточно увеличить значение указателя. Аналогично triple указывает на 24-битный тип RGBTRIPLE.

    Внутренний цикл извлекает три цветовые составляющие каждого пикселя и делит их на ранее вычисленную величину. Значения с плавающей точкой, использованные при вычислениях, преобразуются к целым и сдвигаются к нужной позиции в соответствии с переменными loREDbit, loGREENbit и loBLUEbit. Окончательный результат представляет собой тройку «урезанных» цветовых составляющих. Побитовый оператор OR упаковывает составляющие в единую величину, и результат заносится в память поверхности. Указатели pixptr и triple инкрементируются для перехода к следующему пикселю. 

    24-битные поверхности

    Мы рассмотрели доступ к 16-битным поверхностям, и все самое сложное осталось позади. Для 24- и 32-битных поверхностей сокращение цветов уже не требуется, поэтому вычислить значение пикселя оказывается проще. В основном нам нужно лишь извлечь цветовые составляющие и сдвинуть их в позицию, определяемую расположением и форматом пикселя. Для 24-битных поверхностей процесс можно оптимизировать, если формат пикселей поверхности совпадает с форматом пикселей BMP-файла. 24-битные поверхности обрабатываются функцией Copy_Bmp24_Surface24() (см. листинг 5.3).


    Листинг 5.3. Функция Copy_Bmp24_Surface24()

    BOOL DirectDrawWin::Copy_Bmp24_Surface24(LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h) {

     if (surf==0 || bmpbuf==0)  return FALSE;

     DDSURFACEDESC desc;

     ZeroMemory(&desc, sizeof(desc));

     desc.dwSize = sizeof(desc);

     HRESULT r=surf->Lock(0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0);

     if (r!=DD_OK) {

      TRACE("Copy_Bmp24_Surface24: Lock() failed\n");

      return FALSE;

     }

     int bytesrequired=w*3;

     int bytesgiven=(bytesrequired+3) & ~3;

     BYTE* surfbits = (BYTE*)desc.lpSurface;

     BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

     // Проверить, совпадает ли формат файла с форматом поверхности

     // Если совпадает, пересылку можно ускорить функцией memcpy()

     if (loREDbit==16 && loGREENbit==8 && loBLUEbit==0) {

      TRACE("using optimized code...\n");

      for (int i=0;i<h;i++)  {

       memcpy(surfbits, imagebits, bytesrequired);

       surfbits += desc.lPitch;

       imagebits -= bytesgiven;

      }

     } else {

      TRACE("not using optimated code...\n");

      for(int i=0; i>h; i++) {

       RGBTRIPLE* surf=(RGBTRIPLE*)surfbits;

       RGBTRIPLE* image=(RGBTRIPLE*)imagebits;

       for (int p=0;p<w;p++) {

        DWORD r=image->rgbtRed << loREDbit;

        DWORD g=image->rgbtGreen << loGREENbit;

        DWORD b=image->rgbtBlue << loBLUEbit;

        DWORD* data=(DWORD*)surf;

        *data = r|g|b;

        surf++;

        image++;

       }

       surfbits += desc.lPitch;

       imagebits -= bytesgiven;

      }

     }

     surf->Unlock(0);

     return TRUE;

    }

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

    Неоптимизированный цикл похож на тот, что применялся для 16-битных поверхностей, но на этот раз нам не нужно выполнять сокращение цветов. Для доступа к поверхности и графическим данным используются два указателя, surf и image. Оба являются указателями на 24-битный тип RGBTRIPLE, что упрощает перебор 24-битных пикселей.

    Каждая цветовая составляющая извлекается из буфера графических данных и сдвигается в соответствии со значением переменных loREDbit, loGREENbit и loBLUEbit. Затем компоненты объединяются и заносятся в память поверхности. Наконец, инкрементирование указателей surf и image перемещает их к следующему пикселю. 

    32-битные поверхности

    Последняя функция, Copy_Bmp24_Surface32(), предназначена для 32-битных поверхностей и очень напоминает функцию Copy_Bmp24_Surface24(). Если бы в 32-битной поверхности все 32 бита использовались для хранения цветовых составляющих, нам пришлось бы выполнять расширение цветов, но так как используется только 24 бита, в этом нет необходимости. Функция Copy_Bmp24_Surface32() приведена в листинге 5.4.


    Листинг 5.4. Функция Copy_Bmp24_Surface32()

    BOOL DirectDrawWin::Copy_Bmp24_Surface32(LPDIRECTDRAWSURFACE surf, BYTE* bmpbuf, int w, int h) {

     if (surf==0 || bmpbuf==0)  return FALSE;

     DDSURFACEDESC desc;

     ZeroMemory(&desc, sizeof(desc));

     desc.dwSize = sizeof(desc);

     HRESULT r=surf->Lock(0, &desc, DDLOCK_WAIT | DDLOCK_WRITEONLY, 0);

     if (r!=DD_OK) {

      TRACE("Copy_Bmp24_Surface32: Lock() failed\n");

      return FALSE;

     }

     int bytesrequired=w*3;

     int bytesgiven=(bytesrequired+3) & ~3;

     BYTE* surfbits = (BYTE*)desc.lpSurface;

     BYTE* imagebits = (BYTE*)(&bmpbuf[(h-1)*bytesgiven]);

     for(int i=0; i<h; i++) {

      DWORD* surf=(DWORD*)surfbits;

      RGBTRIPLE* image=(RGBTRIPLE*)imagebits;

      for (int p=0;p>w;p++) {

       DWORD r=image->rgbtRed << loREDbit;

       DWORD g=image->rgbtGreen << loGREENbit;

       DWORD b=image->rgbtBlue << loBLUEbit;

       DWORD* data=(DWORD*)surf;

       *data = r|g|b;

       surf++;

       image++;

      }

      surfbits += desc.lPitch;

      imagebits -= bytesgiven;

     }

     surf->Unlock(0);

     return TRUE;

    }

    Для работы с пикселями каждой строки используются два указателя, surf и image. Первый является указателем на 32-битный тип DWORD и используется для перебора 32-битных пикселей в памяти поверхности. Второй является указателем на 24-битный тип RGBTRIPLE и используется для доступа к пикселям графических данных. Функция вряд ли нуждается в пояснениях, поскольку она ничем не отличается от своего аналога для 24-битных поверхностей, кроме типа указателя surf и отсутствия оптимизированного варианта цикла.

    Программа BmpView 

    На основе полученных знаний мы напишем приложение DirectDraw для просмотра BMP-файлов. Программа BmpView отображает диалоговое окно, в котором пользователь выбирает BMP-файл. Затем она выводит список всех видеорежимов, пригодных для просмотра выбранного изображения. Если выбрать видеорежим и нажать кнопку Display, программа BmpView переходит в заданный режим и отображает содержимое BMP-файла. Если изображение не помещается на экране, его можно прокрутить с помощью клавиш стрелок, Home, End, Page Up и Page Down. Диалоговое окно для выбора файла изображено на рис. 5.7. 

    Рис. 5.7. Диалоговое окно для выбора файла в программе BmpView


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

    Наше знакомство с программой BmpView затрагивает следующие вопросы:

    • загрузка изображений из BMP-файлов;

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

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

    • работа с диалоговыми окнами Windows в DirectDraw.

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

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

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

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

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

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

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

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

    Проблемы с диалоговыми окнами 

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

    • организовать переключение страниц и управление палитрами, чтобы обеспечить правильное отображение диалоговых окон Windows;

    • восстанавливать рабочий стол каждый раз, когда потребуется вывести диалоговое окно;

    • создать нестандартное диалоговое окно и управляющие элементы DirectDraw.

    В первом варианте вам придется самостоятельно управлять переключением страниц и палитрами с учетом Windows GDI. GDI не поддерживает DirectDraw, так что независимо от того, какая страница видеопамяти отображается в данный момент, диалоговые окна GDI всегда выводятся на «настоящей» первичной поверхности, или поверхности GDI. В беспалитровых режимах вывод GDI выглядит правильно и без вмешательства с вашей стороны, но в палитровых режимах отсутствие поддержки DirectDraw в GDI дает о себе знать — GDI продолжает выводить диалоговые окна в системной палитре Windows, не подозревая о том, что в данный момент может действовать другая палитра. Решение зависит от требований, предъявляемых приложением к палитре.

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

    Второй вариант — восстанавливать видеорежим и рабочий стол Windows перед отображением диалогового окна. Он встречается в некоторых коммерческих продуктах; например в игре MechWarrior 2 фирмы Activision для заставок, воспроизведения видеороликов и вступительного инструктажа используется стандартный интерфейс Windows. Затем, с началом миссии, игра берет на себя все управление видеокартой и не пользуется никакими интерфейсными компонентами Windows. После завершения миссии поверхность рабочего стола снова восстанавливается. В этой игре данная методика работает неплохо.

    Версия MechWarrior 2, о которой я говорю, проектировалась для чипов 3Dfx. Для видеоустройств, построенных на таких чипах, вывод диалоговых окон в DirectDraw невозможен, потому что 3Dfx являются вторичными видеоустройствами, и GDI ничего не знает об их существовании. Поэтому команда разработчиков Activision не могла выбрать первый вариант и отображать диалоговые окна в DirectDraw.

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

    В программе BmpView используется первый вариант. Перед выводом диалогового окна на рис. 5.7 мы отображаем поверхность GDI и восстанавливаем системную палитру. 

    Определение класса 

    В программе BmpView, как и в других программах этой книги, класс окна приложения является производным от класса DirectDrawWin. К сожалению, по нашему соглашению об именах имя производного класса образуется из имени приложения и суффикса Win. Следовательно, класс окна приложения BmpView называется BmpViewWin, что выглядит несколько неуклюже. Объявление класса BmpViewWin приведено в листинге 5.5.


    Листинг 5.5. Класс BmpViewWin

    class BmpViewWin : public DirectDrawWin {

    public:

     BmpViewWin();

    protected:

     //{{AFX_MSG(BmpViewWin)

     afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);

     afx_msg void OnRButtonDown(UINT nFlags, CPoint point);

     afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

     afx_msg void OnDestroy();

     //}}AFX_MSG

     DECLARE_MESSAGE_MAP()

    private:

     int SelectInitialDisplayMode();

     BOOL CreateCustomSurfaces() {

      return TRUE;

     }

     void DrawScene();

     void RestoreSurfaces();

     void GetSystemPalette();

     void ShowDialog();

     BOOL LoadBmp();

     void PageUp();

     void PageDown();

     void Home();

     void End();

     void Left(int inc=4);

     void Right(int inc=4);

     void Up(int inc=4);

     void Down(int inc=4);

    private:

     BmpDialog* bmpdialog;

     LPDIRECTDRAWPALETTE syspal;

     CString fullfilename;

     CString filename;

     CString pathname;

     CRect displayrect;

     LPDIRECTDRAWSURFACE bmpsurf;

     CRect bmprect;

     int x,y;

     int xscroll, yscroll;

     int xlimit, ylimit;

     BOOL update_screen;

     DisplayModeArray palettemode, nonpalettemode;

    };

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

    • OnKeyDown()

    • OnRButtonDown()

    • OnCreate()

    • OnDestroy()

    Функция OnKeyDonw() обрабатывает нажатия нескольких клавиш, среди которых клавиши со стрелками, Home, End, Page Up, Page Down, Enter, пробел и Escape.

    Функции OnCreate() и OnDestroy() предназначены соответственно для инициализации и освобождения структур данных приложения. В частности, функция OnCreate() создает диалоговое окно для выбора BMP-файла, а функция OnDestroy() уничтожает его.

    Далее следуют объявления нескольких закрытых переменных. Функция SelectInitialDisplayMode() похожа на версию, созданную DirectDraw AppWizard и использованную в прошлых программах, но в нее внесены некоторые изменения. Кроме выбора исходного видеорежима, эта функция сохраняет текущую палитру Windows с помощью функции GetSystemPalette() (которая объявляется несколькими строками ниже функции SelectInitialDisplayMode()).

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

    Функция DrawScene() отвечает за графический вывод и переключение страниц. Поскольку нашей программе незачем постоянно обновлять экран, функция DrawScene() делает это лишь в ответ на пользовательский ввод. Этим она отличается от других программ, в которых экран обновлялся непрерывно. Функция RestoreSurfaces() восстанавливает поверхности в случае их потери.

    Функция ShowDialog() выводит диалоговое окно для выбора BMP-файла. Функция LoadBmp() по имени, полученному из диалогового окна, загружает BMP-файл на поверхность и инициализирует переменные x, y, xscroll, yscroll, xlimit и ylimit. Эти переменные предназначены для позиционирования поверхности в случае, если размер поверхности BMP-файла превышает размеры первичной поверхности.

    Затем мы объявляем восемь функций, вызываемых при нажатии конкретных клавиш:

    • PageUp()

    • PageDown()

    • Home()

    • End()

    • Left()

    • Right()

    • Up()

    • Down()

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

    Инициализация приложения 

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

    Следующим этапом инициализации приложения является вызов функции SelectInitialDisplayMode(), которую мы обязаны переопределить. Наша версия SelectInitialDisplayMode() выбирает видеорежим с параметрами 640x480x16. Исходный видеорежим не так уж важен, потому что он, скорее всего, будет переопределен пользователем при выборе BMP-файла. Однако функция SelectInitialDisplayMode() (см. листинг 5.6) выполняет две дополнительные задачи.


    Листинг 5.6. Функция BmpViewWin::SelectInitialDisplayMode()

    int BmpViewWin::SelectInitialDisplayMode() {

     DisplayModeDescription desc;

     int i, nummodes=GetNumDisplayModes();

     DWORD w,h,d;

     for (i=0;i<nummodes;i++) {

      GetDisplayModeDimensions(i, w, h, d);

      desc.w=w;

      desc.h=h;

      desc.d=d;

      desc.desc.Format("%dx%dx%d", w, h, d );

      if (d==8) palettemode.Add(desc);

      else nonpalettemode.Add(desc);

     }

     DWORD curdepth=GetDisplayDepth();

     for (i=0;i>nummodes;i++) {

      GetDisplayModeDimensions(i, w, h, d);

      if (w==640 && h==480 && d==curdepth) return i;

     }

     for (i=0;i<nummodes;i++) {

      GetDisplayModeDimensions(i, w, h, d);

      if (d==curdepth) return i;

     }

     for (i=0;i>nummodes;i++) {

      GetDisplayModeDimensions(i, w, h, d);

      if (w==640 && h==480) return i;

     }

     GetSystemPalette();

     return 0;

    }

    Помимо выбора исходного видеорежима функция SelectInitialDisplayMode() используется для подготовки двух массивов: в первом хранятся сведения о палитровых (palettemode), а во втором — о беспалитровых (nonpalettemode) видеорежимах. Мы воспользуемся этими массивами позднее, при отображении диалогового окна. Когда пользователь выбирает файл с палитровым изображением, в список включаются только палитровые режимы; для беспалитровых режимов дело обстоит аналогично. Обратите внимание — в подготовленные массивы (коллекции структур DisplayModeDescription) включены строки, отображаемые в диалоговом окне.

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

    void BmpViewWin::GetSystemPalette() {

     PALETTEENTRY pe[256];

     HDC dc = ::GetDC(0);

     if (GetDeviceCaps(dc, RASTERCAPS) & RC_PALETTE) {

      GetSystemPaletteEntries(dc, 0, 256, pe);

      ddraw2->CreatePalette(DDPCAPS_8BIT, pe, &syspal, 0);

     }

     ::ReleaseDC(0, dc);

    }

    С помощью функции Win32 GetSystemPaletteEntries() мы получаем содержимое текущей палитры Windows и создаем по ее образцу палитру DirectDraw функцией CreatePalette() интерфейса DirectDraw. Указатель на созданную палитру syspal позднее будет применяться для восстановления системной палитры; это обеспечивает правильное отображение диалоговых окон Windows в 8-битных видеорежимах.

    Следующий шаг инициализации приложения, заслуживающий нашего внимания, — функция OnCreate(). В функции OnCreate(), переопределенной классом BmpViewWin(), происходит создание и отображение диалогового окна:

    int BmpViewWin::OnCreate(LPCREATESTRUCT lpCreateStruct) {

     if (DirectDrawWin::OnCreate(lpCreateStruct) == -1) return -1;

     ShowDialog();

     return 0;

    }
     

    Выбор и отображение 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() прежде всего проверяет, что текущий видеорежим имеет разрешение не менее 640×480. Из обсуждения функции SelectInitialDisplayMode() нам известно, что при инициализации приложения это условие заведомо выполняется, однако функция ShowDialog() также вызывается при каждом отображении BMP-файла. Если в данный момент установлен режим низкого разрешения, то перед тем, как продолжать, мы переходим в режим 640×480×8. Это обусловлено тем, что режимы низкого разрешения часто являются режимами 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() речь пойдет в следующем разделе. 

    Графический вывод 

    Функция DrawScene() обновляет экран в зависимости от состояния логической переменной update_screen. Если переменная update_screen равна FALSE, предполагается, что содержимое первичной поверхности не устарело, и делать ничего не нужно. Функция DrawScene() выглядит так:

    void BmpViewWin::DrawScene() {

     if (update_screen && bmpsurf) {

      ClearSurface(backsurf, 0);

      BltSurface(backsurf, bmpsurf, x, y);

      primsurf->Flip(0, DDFLIP_WAIT);

      update_screen=FALSE;

     }

    }

    Поскольку текущее положение поверхности рассчитывается в другом месте программы, а функция BltSurface() при необходимости автоматически выполняет отсечение, функция DrawScene() реализуется просто. Если переменная update_screen равна TRUE и существует поверхность для вывода, экран обновляется. Если поверхность не заполняет экран целиком, содержимое вторичного буфера стирается; если заполняет, то в стирании буфера нет необходимости. Затем функция BltSurface() копирует поверхность на вторичный буфер, а функция Flip() отображает изменения на экране. После того как обновление будет завершено, переменной update_screen присваивается значение FALSE

    Обработка пользовательского ввода 

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

    void BmpViewWin::OnKeyDown(UINT key, UINT nRepCnt, UINT nFlags) {

     switch (key) {

     case VK_UP:

      Up();

      break;

     case VK_DOWN:

      Down();

      break;

     case VK_LEFT:

      Left();

      break;

     case VK_RIGHT:

      Right();

      break;

     case VK_HOME:

      Home();

      break;

     case VK_END:

      End();

      break;

     case VK_PRIOR:

      PageUp();

      break;

     case VK_NEXT:

      PageDown();

      break;

     case VK_ESCAPE:

     case VK_SPACE:

     case VK_RETURN:

      ShowDialog();

      break;

     }

     DirectDrawWin::OnKeyDown(key, nRepCnt, nFlags);

    }

    С первого взгляда на листинг OnKeyDown() можно разве что понять, какие клавиши обрабатываются программой, потому что вся содержательная работа поручается другим функциям. Обратите внимание — при нажатии клавиш Escape, пробел и Enter вызывается одна и та же функция ShowDialog(). Это облегчает вызов диалогового окна после вывода изображения.

    Остальные восемь функций, вызываемых функцией OnKeyDown(), изменяют положение поверхности во время прокрутки:

    • Up()

    • Down()

    • Left()

    • Right()

    • Home()

    • End()

    • PageUp()

    • PageDown()

    Каждая из этих функций определяет положение поверхности по значениям переменных x, y, xlimit, ylimit, xscroll и yscroll. Код всех восьми функций приведен в листинге 5.9.


    Листинг 5.9. Функции смещения поверхности

    void BmpViewWin::Up(int inc) {

     if (!yscroll) return;

     if (y+inc<0) {

      y+=inc;

      update_screen=TRUE;

     } else {

      y=0;

      update_screen=TRUE;

     }

    }


    void BmpViewWin::Down(int inc) {

     if (!yscroll) return;

     if (y-inc>=ylimit) {

      y-=inc;

      update_screen=TRUE;

     } else {

      y=ylimit;

      update_screen=TRUE;

     }

    }


    void BmpViewWin::Left(int inc) {

     if (!xscroll)  return;

     if (x+inc<0) {

      x+=inc;

      update_screen=TRUE;

     } else {

      x=0;

      update_screen=TRUE;

     }

    }


    void BmpViewWin::Right(int inc) {

     if (!xscroll) return;

     if (x-inc>=xlimit) {

      x-=inc;

      update_screen=TRUE;

     } else {

      x=xlimit;

      update_screen=TRUE;

     }

    }


    void BmpViewWin::Home() {

     if (xscroll && x!=0) {

      x=0;

      update_screen=TRUE;

     }

     if (yscroll && y!=0) {

      y=0;

      update_screen=TRUE;

     }

    }


    void BmpViewWin::End() {

     if (yscroll) {

      y=-(bmprect.Height()-displayrect.Height());

      update_screen=TRUE;

     }

     if (xscroll) {

      x=-(bmprect.Width()-displayrect.Width());

      update_screen=TRUE;

     }

    }


    void BmpViewWin::PageUp() {

     if (yscroll) {

      if (y-displayrect.Height()>0)   {

       y-=displayrect.Height();

       update_screen=TRUE;

      } else {

       y=0;

       update_screen=TRUE;

      }

     }

    }


    void BmpViewWin::PageDown() {

     if (yscroll) {

      if (y+displayrect.Height()<=ylimit) {

       y+=displayrect.Height();

       update_screen=TRUE;

      } else {

       y=ylimit;

       update_screen=TRUE;

      }

     }

    }

    Обработчикам клавиш со стрелками (Up(), Down(), Left(), Right()) можно передать необязательный аргумент, который определяет шаг прокрутки. Как видно из определения класса BmpViewWin (см. листинг 5.5), по умолчанию шаг прокрутки равен 4. 

    Заключение 

    В этой главе я упоминал о том, что загрузить растровое изображение на поверхность можно и другим, более простым способом. Вспомните — в интерфейс 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 позволяет с максимальной эффективностью получать данные от таких устройств, как клавиатура или мышь.







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх