GMSAPR
.pdf91
Класс CRaster содержит два метода DrawBitmap, выполняющих вывод изображения на контекст устройства. Аргументы одного из методов позволяют задать положение и размеры выводимой области исходного изображения и определить область назначения. По умолчанию изображение выводится полностью в масштабе 1:1, однако с помощью аргументов этой функции можно и изменить масштаб. Второй метод позволяет просто указать позицию начала вывода и масштаб, в котором должно быть нарисовано изображение. Оба метода внутри используют мощную API-функцию StretchDIBits(). Начиная с Windows 98, реализация этой функции умеет выводить растровые данные в форматах JPEG и PNG. Класс CDC, который мы обычно используем для рисования, имеет похожий метод CDC::StretchBlt(), но он в качестве исходного изображения просит указать контекст устройства, а не указатель на данные.
Режим масштабирования выбирается CDC-методом int
SetStretchBltMode( int nStretchMode )
Аргумент функции — iStretchMode — режим масштабирования. Поддерживаются следующие режимы масштабирования:
•BLACKONWHITE — выполняет булеву операцию AND между цветом существующих и удаленных пикселов (при уменьшении размера изображения). Этот режим используется, если масштабируется рисунок «черным по белому», то есть алгоритм масштабирования будет стараться сохранить черные пикселы;
•COLORONCOLOR — этот режим удаляет (добавляет) строки (столбцы) пикселов без каких-либо попыток сохранить содержащуюся в них информацию. Наиболее быстрый режим. Используется, когда необходимо сохранить цвета изображения неизменными;
•WHITEONBLACK — выполняет булеву операцию OR. Этот режим используется, если масштабируется рисунок «белым по черному»;
92
•HALFTONE — преобразует изображение к заданному размеру и при этом трансформирует цвета так, чтобы средний цвет полученной картинки приближался к исходному цвету. Наиболее медленный режим. Однако, масштабированная картинка выглядит
лучше за счет сглаживания «лестничного эффекта».
При масштабировании фотографий и цветных рисунков в большинстве случаев наиболее подходящим является режимы COLORONCOLOR и HALFTONE. Далее мы рассмотрим на практике различия между этими режимами.
Метод GetHistogram() предназначен для получения гистограммы яркости изображения. О том, что это такое и зачем "оно" нужно, мы поговорим дальше.
Поскольку данными (документом) в нашей программе будут изображения, на следующем шаге модифицируем класс документа так, чтобы он умел работать с изображениями.
4.5Модификация класса документа для обеспечения работы с изображениями
Прежде всего, надо решить, что является данными, которыми будет управлять класс документа. Данными в нашем случае будет изображение, а для изображений мы завели класс CRaster, значит, в классе документа надо определить данные как объекты класса CRaster. В принципе, для целей показа картинки на экране хватит и одного объекта CRaster. Однако, мы собираемся в дальнейшем наделить программу некоторыми возможностями по редактированию изображений, поэтому нам потребуется не один, а, как минимум, два объекта: один для хранения исходной картинки, второй — буфер для приема преобразованной картинки.
Порядок работы с двумя объектами CRaster в этом случае будет выглядеть следующим образом.
93
Загружаем изображение в первый объект CRaster и показываем его на экране до тех пор, пока пользователь не даст команду выполнить какие-нибудь изменения изображения. После этого помещаем измененное изображение во второй объект CRaster и начинаем показывать второй модифицированный объект-картинку.
Может случиться так, что пользователю не понравится то, как мы изменили его картинку, тогда он отдает команду «Отменить преобразования». Легко — просто меняем объекты местами.
Итак, заведем в классе нашего документа CBMDoc пару объектов
CRaster, которые и будут хранить изображения:
CRaster m_BM[2]; // два буфера для изображений
CRaster *m_pCurBM; // указатель на активный буфер
Указатель m_pCurBM будет хранить адрес текущего изображения, его-то мы и будем показывать.
Для загрузки изображения переопределим метод OnOpenDocument()
класса CBMDoc. Надо отметить, что каркас приложения при запуске программы автоматически создает пустой новый документ. Чтобы этого не происходило, переопределим в классе CBMApp метод-обработчик сообщения команды
ID_FILE_NEW и оставим тело этого метода пустым. Если же вы захотите наделить программу функцией создания нового документа, то придется как-то иначе обрабатывать эту команду.
BOOL CBMDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
if (!CDocument::OnOpenDocument(lpszPathName)) return FALSE;
// Загружаем в первый буфер if(m_BM[0].LoadBMP(lpszPathName))
{
94
m_pCurBM=&m_BM[0];
//Умеем редактировать только RGB888 (RGB24) данные if(m_pCurBM->GetBMInfoPtr()-> bmiHeader.biBitCount!=24)
m_bEditable=FALSE; else
m_bEditable=TRUE;
return TRUE;
}
return FALSE;
}
Как вы можете видеть из листинга, изображение загружается в первый из объектов CRaster. Этот объект становится текущим, его адрес запоминаем в переменной m_pCurBM. Далее проверяем формат цвета изображения. Если он не равен RGB888, то ставим флажок m_bEditable в значение FALSE. Это вовсе не означает, что картинки с отличающимся от RGB888 форматом цвета не будут показываться нашей программой, просто те функции по редактированию изображений, которые мы добавим далее, будут ориентированы на работу с RGB888, а флаг m_bEditable будет предостерегать от их неправильного использования. Если у вас появится желание редактировать не RGB888-картинки, то вам придется либо переделать функции преобразований, либо конвертировать картинки в формат RGB888 на этапе загрузке из файла.
4.6Заключение
Вданной главе мы разобрались, каким образом хранятся растровые рисунки в BMP-формате и рассмотрели его структуру. Рассмотрели создание
MDI-приложения в Microsoft Visual C++ 2005 .NET и реализовали класс
CRaster. Благодаря нему, теперь любое приложение сумеет экспортировать свои рисунки в растровый формат BMP. Рисунки сохраняются в том виде, в
95
котором они представлены на экране. В принципе, можно ограничиться сохранением только той части рисунка, в которой имеется изображение.
В реальных программах, как правило, не имеет смысла писать собственные функции или классы сохранения/загрузки изображений, достаточно воспользоваться платными или бесплатными библиотеками (таких как FreeImage и другие). При использовании классов GDI+ можно воспользоваться встроенными в ОС Windows кодеками (codec – сочетание слов coder и decoder, то есть механизм, умеющий как кодировать, так и декодировать некоторую информацию).
96
5Двойная буферизация и вывод изображения
Впрограммировании графических приложений, когда приходиться иметь дело с выводом некоторых рисунков на экран (графиков, схем и прочее), широко используется концепция «виртуального экрана» (известная так же как двойная буферизация) — скрытого экрана, на котором происходит подготовка изображения к показу на мониторе. Подготовка может занимать относительно длительное время и включать в себя ряд операций, которые вовсе не обязательно показывать пользователю. Копирование же из виртуального экрана на реальный происходит в одно действие, гораздо быстрее, чем построение картинки непосредственно на экране.
5.1Реализация виртуального экрана в программе BMViewer
Внашем случае изображение может быть выведено на контекст устройства с помощью метода CRaster::DrawBitmap(), внутри же этого метода используется API-функция StretchDIBits(), которая всем хороша, однако работает сравнительно медленно. Поэтому поэксплуатируем идею виртуального экрана. Создадим в программе такой экран, и будем выводить изображение не очень часто, а только тогда, когда изменим изображение. Более часто перерисовывать изображение придется объекту-облику, например, при обработке сообщений прокрутки, при перерисовке окна после перекрытия другими окнами или при изменении размеров окна, в этом случае будем просто копировать изображение из виртуального экрана на контекст устройства дисплея с помощью метода CDC::BitBlt(), который работает относительно быстро.
Виртуальный экран может быть реализован по-разному. Далее мы рассмотрим реализацию, при которой размер экрана равен размеру масштабированной картинки. Достоинство такого подхода в том, что на виртуальном экране мы можем разместить полностью готовую для вывода
97
картинку. В случае, если картинка целиком не помещается в окно вывода, достаточно просто сместиться на позицию прокрутки и отобразить в окне нужную часть изображения.
Недостаток — требуется большое количество памяти под растр виртуального экрана. Допустим, у нас имеется изображение 100 на 100 пикселов, при глубине цвета 32 бита растр виртуального экрана будет занимать 40 Кбайт, а при масштабировании рисунка в 10 раз и создании виртуального экрана такого же размера его растр будет занимать уже 4 Мбайта. Такими темпами легко могут возникнуть проблемы с выводом больших рисунков.
В принципе, можно и не держать большой виртуальный экран. Вполне достаточно экрана размером с максимально возможное окно вывода. В этом случае нам потребуется лишь отображать на этот экран нужную часть изображения.
Достоинство такого подхода в том, что мы экономим память и можем масштабировать изображения, не опасаясь нехватки памяти под виртуальный экран.
Недостаток — нам придется предпринять дополнительные действия по обработке сообщений о прокрутке изображения или изменении размеров окна, и каждый раз при поступлении таких сообщений выполнять вывод на виртуальный экран требуемой части изображения. Это может замедлить прокрутку изображений.
Поэтому, при разработке реальных приложений необходимо взвесить факторы — размеры растровых данных для визуализации, возможность масштабирования и прокрутки и пр. и сделать соответствующий выбор.
Поскольку за визуализацию изображений в нашей программе ответственен класс облика CBMView, возложим на него также и обязанности по обслуживанию виртуального экрана. Изменения, которые надо сделать, рассмотрены в следующем разделе.
98
5.2 Модификация класса облика
Для реализации виртуального окна заведем в объекте-облике пару переменных:
CBitmap m_VirtScreenBitmap;
CDC m_VirtScreenDC;
В объекте m_VirtScreenBitmap будем хранить растр виртуального экрана, а объект m_virtScreenDC будет контекстом виртуального экрана.
Контекст виртуального экрана должен быть совместим с контекстом окна, в который будет выполняться вывод. Добавим в класс CBMView обработчик сообщения WM_CREATE, в котором и создадим совместимый контекст:
int CBMView::OnCreate( LPCREATESTRUCT lpCreateStruct)
{
if (CScrollView::OnCreate(lpCreateStruct)==-1) return -1;
// Создадим совместимый контекст для виртуального экрана
CPaintDC dc(this); m_VirtScreenDC.CreateCompatibleDC(&dc);
return 0;
}
Теперь нам остается только организовать вывод изображения на виртуальный экран. Для решения этой задачи нам потребуется сделать следующее:
Добавить метод, в котором на виртуальный экран будет выводиться изображение, содержащееся в объекте-документе. Назовем его UpdateVirtualScreen(). В этом же методе будем создавать растр для виртуального экрана, достаточный по размерам для вывода всего изображения.
99
В метод CBMView::OnDraw() добавим копирование виртуального экрана в клиентскую часть окна вывода. Копирование будем выполнять с учетом позиции прокрутки.
Переопределить виртуальный метод OnUpdate() так, чтобы в нем устанавливались размеры области прокрутки, соответствующие размеру изображения с учетом коэффициента масштабирования. Это требуется для того, чтобы пользователь мог, прокручивая изображение в окне, полюбоваться на любую его часть. Добавленный же при создании каркаса приложения метод OnInitialUpdate() можно удалить (но тогда не забыть убрать и объявление этого метода в интерфейсе класса) или просто закомментировать в нем действия по установке размеров области прокрутки. Это можно сделать, так как реализация OnInitialUpdate() в базовом классе вызывает метод
OnUpdate(), который мы уже модифицировали должным образом.
Ещё нужно обязательно переопределить метод-обработчик сообщения WM_ERASEBKGND для того, чтобы самим контролировать перерисовку (очистку) фона окна облика – в переопределенном методе необходимо закомментировать вызов базового метода, оставив лишь return 0. Это позволит избежать мерцания при перерисовке окна облика.
Об этом шаге многие часто забывают, и использование виртуальных экранов даёт мало пользы – при перерисовке возникает частое мерцание: система сначала заливает экран белым фоном, и только потом копируется содержимое виртуального экрана.
// Цвет для заливки фона
#define GRAY RGB(127, 127, 127) BOOL CBMView::UpdateVirtualScreen()
{
CBMDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// Получили указатель на активную картинку
100
CRaster* pCurBM=pDoc->GetCurrentBMPtr(); if(pCurBM==NULL) return FALSE;
// Вычисляем размеры картинки с учетом масштаба
LONG imgw=static_cast<LONG> (pCurBM-> GetBMWidth()*m_dScale); LONG imgh=static_cast<LONG> (pCurBM-> GetBMHeight()*m_dScale);
// Если битмап существует, возьмем ее размер
BITMAP BMStruct;
BMStruct.bmWidth=BMStruct.bmHeight=0;
if(m_VirtScreenBitmap.GetSafeHandle( )) m_VirtScreenBitmap.GetBitmap(&BMStruct);
//Если размеры виртуального экрана
//меньше размеров картинки, увеличим экран
if(BMStruct.bmWidth<imgw || BMStruct.bmHeight<imgh)
{
CPaintDC dc(this);
// Размеры дисплея в пикселах
int scrw=dc.GetDeviceCaps(HORZRES); int scrh=dc.GetDeviceCaps(VERTRES);
//Выберем временную битмап в контексте
//это освободит m_VirtScreenBitmap
//если она была ранее выбрана в контексте
//и даст возможность удалить ее
CBitmap TempBM; TempBM.CreateCompatibleBitmap(&dc,1,1); m_VirtScreenDC.SelectObject(&TempBM);
// Разрушим ранее существовавшую битмап m_VirtScreenBitmap.DeleteObject();
//и на ее месте построим новую, по размерам изображения
//не меньше размеров дисплея
if(!m_VirtScreenBitmap.CreateCompatibleBitmap(&dc,
(imgw<scrw?scrw:imgw), (imgh<scrh?scrh:imgh)))