Программирование на Visual С++ - No.76 (GDI+ ч.2 - растровые изображения)
Информационный Канал Subscribe.Ru |
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте, дорогие подписчики! CТАТЬЯ GDI+ Автор: Виталий БрусенцевНеобходимые замечания к предыдущей частиПрежде всего, я благодарю всех читателей, которые откликнулись на первую часть статьи со своими замечаниями и комментариями. Похоже, что затронутая тема интересует многих программистов, решающих задачи обработки графики. В этом разделе я постарался ответить на многие вопросы, возникшие при обсуждении GDI+. Данная статья вовсе не заменяет документацию Platform SDK, а, напротив, пытается восполнить ее пробелы на основе собственных экспериментов и общения в Usenet (преимущественно в группах новостей microsoft.public.dotnet.framework.drawing и microsoft.public.win32programmer.gdi). Несколько слов о производительностиБольшое количество обсуждений было связано с вопросами производительности GDI+. Появились даже "экстремальные" суждения о полной непригодности этой графической библиотеки как чрезвычайно медлительного средства. Немалую роль в таком отношении сыграло и молчание MS в ответ на многочисленные вопросы о поддержке в GDI+ средств аппаратной акселерации. Представители Microsoft Developer Support лишь туманно отвечали, что, дескать, сравнивать производительность GDI+ и GDI не совсем корректно (читай: GDI+ не выигрывает от сравнения) и что версия 1.0 не имеет собственного Device Driver Interface (читай: аппаратная акселерация отсутствует). В последнее время заявления представителей Microsoft стали вселять большую надежду. Было сказано, что "команда разработки GDI+ прекрасно знает о том, что производительность требует улучшения во многих областях" и что "производительность – это то, над чем сейчас работает команда GDI+". В компьютерном сообществе немедленно начали циркулировать слухи о том, что аппаратная акселерация будущих версий GDI+ связана с выходом DirectX 9. Ну что же, поживем – увидим… Со своей стороны скажу следующее. Во-первых, вычислительная мощность современных PC уже достигла того уровня, когда 90% ресурсов процессора офисное приложение может позволить себе тратить на рисование. В профессиональных пакетах компьютерной графики такие же изобразительные средства достигаются тяжким трудом программистов (и по цене примерно такой же скорости). Впервые средство создания офисной графики такого качества попало в руки массовых разработчиков. И неразумно отказываться, например, от удобной поддержки графических форматов в GDI+ только из-за того, что вас не устраивает в ней, скажем, скорость вывода примитивов. Кроме того, GDI+ и не создавалась как "игровой движок" (для этого существуют такие мощнейшие API, как DirectX и OpenGL), а во многих других областях ее производительности вполне хватает – взгляните хотя бы на интерфейс Windows XP. Если же еще грамотно использовать возможности оптимизации этой библиотеки (постараюсь сказать и об этом), то можно поднять ее производительность до более высокого уровня. Не стоит забывать и про возможности GDI – в тех областях, где производительность и/или функциональность GDI+ оставляет желать лучшего, можно использовать эти средства вместе. К этой теме я еще буду возвращаться, сейчас лишь приведу полезную ссылку на Microsoft Knowledge Base: INFO: Interoperability Between GDI and GDI+ (Q311221). ПоправкаСразу несколько человек указали на то, что в демонстрационных примерах содержится не совсем корректный код перерисовки. Рассмотрим проблему на примере WinAPI-реализации (хотя она имеется и в версии для .NET). Действительно, обработчик сообщения WM_PAINT окна приложения выглядит так: case WM_PAINT: hdc = BeginPaint(hWnd, &ps); OnPaint(hdc, ps.rcPaint); EndPaint(hWnd, &ps); break; Такой подход будет работать, пока окно перерисовывается целиком. Но, если была временно скрыта, а затем показана часть окна (например, при открытии и закрытии пункта меню), то произойдет неприятность. Windows оптимизирует процесс перерисовки, передавая в структуре ps.rcPaint лишь координаты области, нуждающейся в обновлении. Код обработчика передаст эти же координаты в функцию OnPaint, которая послушно воспроизведет в этой маленькой области копию всего большого окна. Очевидно, что это совсем не то, что нужно.
Для того чтобы учитывать частичную перерисовку, есть два общих подхода. Первый означает, что мы доверяем Windows операции отсечения (clipping) и передаем в OnPaint координаты всей клиентской области окна (изменения выделены в листинге): case WM_PAINT: { RECT rc; GetClientRect(hWnd, &rc); hdc = BeginPaint(hWnd, &ps); OnPaint(hdc, rc); EndPaint(hWnd, &ps); break; } Замечу, что для эффективного отсечения GDI+ необходимо модифицировать и код функции OnPaint (добавить вызов метода Graphics::SetClip с координатами области перерисовки). При этом все операции рисования остаются неизменными, а необходимость их выполнения определяется ядром GDI+. Другой подход является более правильным, но он сложен в реализации, и мы его рассматривать здесь не будем. Он подразумевает самостоятельное определение только тех действий, которые необходимы для частичной перерисовки. При этом можно добиться значительного выигрыша в скорости (например, отказавшись от загрузки шрифта, если выводимый текст не попадает в область вывода). Динамическая загрузка GdiPlus.dllВозник довольно интересный вопрос: как можно избежать системного сообщения об ошибке и продолжить работу с использованием GDI, если система, в которой запущено приложение, не поддерживает GDI+? Ведь, в отличие от большинства обычных библиотек импорта, "начинка" GDI+ спрятана за структурой классов-оберток… Здесь может помочь такая сравнительно малоизвестная возможность Visual C++, как отложенная загрузка (Delayed Loading) DLL. При использовании этой опции компоновщик генерирует специальные заглушки для всех импортируемых функций. Приложение стартует немного быстрее, а реальная загрузка библиотеки откладывается (отсюда и название) до первого вызова любой импортируемой функции. Подробно почитать об этой технике можно в декабрьском выпуске MSJ за 1998 год сразу в двух колонках: "Under The Hood" Мэтта Питрека и "QnA Win32" Джефри Рихтера, здесь же только упомяну, что для использования отложенной загрузки необходимо включать библиотеку DELAYIMP.LIB. Скрыть детали использования DLL и инициализации GDI+ можно, используя примерно такой класс: class CGdiPlusInit { public: CGdiPlusInit(); virtual ~CGdiPlusInit(); bool Good(){ return present; } private: bool present; ULONG_PTR token; }; Реализация класса CGdiPlusInit // GdiPlusInit.cpp: implementation of the CGdiPlusInit class. ////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "GdiPlusInit.h" #include <GdiPlus.h> #pragma comment(lib, "gdiplus.lib") #ifndef _DEBUG #pragma comment(lib, "delayimp.lib") #pragma comment(linker, "/delayload:gdiplus.dll") #endif CGdiPlusInit::CGdiPlusInit() { present=true; Gdiplus::GdiplusStartupInput input; __try { Gdiplus::GdiplusStartup(&token, &input, 0); } __except(1) { present=false; } } CGdiPlusInit::~CGdiPlusInit() { if(present) Gdiplus::GdiplusShutdown(token); } Таким образом, этот класс самостоятельно занимается инициализацией/очисткой GDI+, а также обрабатывает структурное исключение, возникающее при отсутствии библиотеки Gdiplus.dll. Для инициализации библиотеки достаточно объявить экземпляр такого класса. Этот способ используется в примерах к статье.
Поддержка Windows 95Что касается технической стороны вопроса, то эксперименты показали: установка дистрибутива gdiplus.dll в системах с Windows 95 возможна, и демонстрационные приложения с использованием GDI+ выполняются без заметных проблем. С правовой же точки зрения, этого делать нельзя. Microsoft достаточно жестко указывает в лицензионном соглашении дистрибутива, что он может устанавливаться только на следующие операционные системы: «Windows 2000, Windows Millennium Edition, Windows NT 4.0 and Windows 98». Как уже говорилось, Windows XP располагает собственной версией GDI+. С чем связано такое ограничение – можно только догадываться. Возможно, с тем, что компания из Редмонда вообще прекратила техническую поддержку Windows 95. Что нового?За время, прошедшее с выхода первой части, произошли некоторые изменения.
Похоже, введение затянулось. Ну что же, перейдем, наконец, к теме этой части. Класс Bitmap - контейнер растровых изображенийЕсли вы помните диаграмму классов GDI+ из предыдущей части, то уже знаете, что класс Image является общим предком для классов Bitmap и Metafile, реализующих, соответственно, функциональность растровых и векторных изображений. Кстати, в документации .NET Framework SDK утверждается, что потомком Image является и класс Icon, но это не так: он унаследован от System.MarshalByRefObject. В иерархии классов GDI+ для C++ класса Icon не существует (все необходимые средства для работы с иконками находятся в Bitmap). Итак, в этой части статьи мы постараемся хорошенько изучить класс Bitmap и особенности работы с ним. Такое внимание к единственному классу вполне обосновано: во-первых, он предоставляет очень много возможностей, а, во-вторых, без использования Bitmap в GDI+ вообще невозможно работать с растровой графикой. Даже если вы создаете кисть с растром (TextureBrush) или контекст устройства (Graphics) в памяти, для их инициализации потребуется экземпляр Bitmap. Поддержка основных графических форматовЭто качество является одним из наиболее привлекательных свойств библиотеки GDI+. Например, скромный и неприметный редактор Paint в Windows XP неожиданно приобрел возможность открывать и сохранять не только картинки BMP, но также и JPG, TIF, GIF и PNG, что сразу сделало его на порядок более мощным средством. Это полезное качество появилось в нем благодаря использованию GDI+ (Paint из комплекта Windows 2000 тоже поддерживал GIF и JPG, но делал это заметно хуже, используя набор модулей расширения FLT для Office 97). К графическим фильтрам GDI+ уже прочно прикрепилось жаргонное название "кодек" (codec, Compressor/Decompressor). Чтобы не отстать от моды, будем их так называть и мы. У класса Bitmap существует набор перегруженных конструкторов для создания растра из всевозможных источников. При создании объекта Bitmap, например, из файла, анализируется его формат (а вовсе не расширение!), и автоматически используется соответствующий кодек. Определить, кодек какого формата был использован для загрузки, можно при помощи метода Image::GetRawFormat: Bitmap bm(L"Picture.dat"); // обратите внимание на расширение GUID guid; bm.GetRawFormat(&guid); // проверим, действительно ли GUID формата соответствует JPEG if(guid == ImageFormatJPEG) MessageBox(0, "Это JPEG!", 0, MB_OK); Константы форматов вида ImageFormatJPEG и т.д. определены в заголовочных файлах GDI+. Только не спутайте их с GUID кодеков, которые будут обсуждаться в следующей части статьи. Для .NET класс Image также поддерживает эту технику, предоставляя свойство RawFormat только для чтения: public ImageFormat RawFormat {get;} Кроме конструкторов, для создания растров можно использовать семейство статических методов FromXXXX класса Bitmap. Они возвращают указатель на объект, который в конце работы необходимо удалять при помощи оператора delete. Соответствующие статические методы имеются и у класса System.Drawing.Bitmap в среде Microsoft .NET Framework. Разумеется, возвращаемый ими объект не нуждается в ручном удалении – об этом позаботится сборщик мусора: { Bitmap bm; . . . bm = Bitmap.FromFile("photo.jpg"); . . . } // здесь bm будет освобожден Каждый кодек по мере возможностей учитывает специфические качества своего формата – например, загрузчик GIF правильно прочитывает прозрачные GIF89a, чего так не хватало функции OleLoadPicturePath. Имеется также возможность сохранения созданных растров в графических файлах различных форматов. Более подробно работа с кодеками будет рассматриваться в Части III, здесь же узнаем о некоторых особенностях загрузки и отображения растров. Загрузка из файлов и потоков IStreamИтак, для загрузки графического файла в экземпляр класса Bitmap существует следующий конструктор: Bitmap( const WCHAR* filename, BOOL useIcm ); Параметр filename должен содержать имя существующего файла. Как уже отмечалось, все строковые параметры методов GDI+ требуют UNICODE-строк, поэтому при передаче строковой константы в программе на C++ необходимо предварять ее префиксом ‘L’. Параметр useIcm определяет, будет ли при загрузке растра использоваться ICM (Image Color Management) и по умолчанию равен FALSE. Если же использовать ICM необходимо, графический файл должен содержать всю необходимую информацию, например, цветовые профили конкретных устройств. Кстати, Bitmap вовсе не унаследовал способность чтения графических файлов от класса Image, как может показаться. Он реализует этот конструктор самостоятельно, вызывая внутри функции GdipCreateBitmapFromFile или GdipCreateBitmapFromFileICM (разумеется, при этом используются одни и те же кодеки, что и конструктором класса Image). Существует достаточно неприятная ошибка в файловом загрузчике форматов – часто (но не всегда), при указании несуществующего имени файла, вместо того, чтобы вернуть код ошибки в переменной Status, приложение завершается с выдачей примерно такого сообщения (на самом деле, в системах Windows 9x окно сообщения об ошибке выглядит еще уродливее):
Мне так и не удалось пресечь такое поведение. Это странно, ведь такая же ситуация в .NET обрабатывается без проблем: using System; using System.Drawing; class Test { public static void Main() { try { Bitmap bm = new Bitmap("NotFound.gif"); } catch(Exception e) { Console.WriteLine("Exception caught!"); // выполнится эта строка } } } Microsoft признает наличие этой ошибки, и в скором времени планируется выход заплатки (и очередной статьи в Knowledge Base). Сейчас единственный разумный способ избежать такого исхода – это убедиться в существовании файла перед его открытием.
Более гибкие возможности загрузки таятся в таком конструкторе Bitmap: Bitmap( IStream* stream, BOOL useIcm ); Он позволяет в качестве источника растровых данных использовать все, для чего можно получить интерфейс IStream на чтение. Это дает нам возможность загружать изображения из баз данных, структурированных хранилищ и блоков памяти – достаточно написать класс, реализующий 3 метода IStream: Seek, Read, и Stat. При этом также анализируется формат данных и используется необходимый кодек для распаковки, если это необходимо. Создание растров из ресурсов программыСледующий конструктор Bitmap позволяет загрузить растр из ресурсов: Bitmap( HINSTANCE hInstance, const WCHAR* bitmapName ); Но не обольщайтесь, далеко не всякий ресурс удастся загрузить таким образом. Этот конструктор предназначен для загрузки именно BITMAP-ресурсов, и не поддерживает, скажем, загрузку GIF. Возможно, это ограничения реализации. К счастью, их достаточно легко обойти: просто предоставьте загрузчику интерфейс IStream с необходимыми данными. Это можно сделать, например, воспользовавшись функцией Windows API CreateStreamOnHGlobal: Bitmap* BitmapFromResource(HINSTANCE hInstance, LPCTSTR szResName, LPCTSTR szResType) { HRSRC hrsrc=FindResource(hInstance, szResName, szResType); if(!hrsrc) return 0; // "Ненастоящий" HGLOBAL – см. MSDN HGLOBAL hg1=LoadResource(hInstance, hrsrc); DWORD sz=SizeofResource(hInstance, hrsrc); void* ptr1=LockResource(hg1); HGLOBAL hg2=GlobalAlloc(GMEM_FIXED, sz); // Копируем растровые данные CopyMemory(LPVOID(hg2), ptr1, sz); IStream *pStream; // TRUE означает освободить память в деструкторе HRESULT hr=CreateStreamOnHGlobal(hg2, TRUE, &pStream); if(FAILED(hr)) return 0; // Используем загрузку из IStream Bitmap *image=Bitmap::FromStream(pStream); // При уничтожении потока память будет освобождена pStream->Release(); return image; }
При загрузке графических ресурсов программы, написанной для .NET, можно также использовать загрузку из объекта System.IO.Stream. Получить экземпляр объекта Stream из ресурса позволяет следующий метод класса System.Reflection.Assembly: public virtual Stream GetManifestResourceStream( string name ); В таком случае загрузка изображения из ресурсов будет выглядеть примерно так: Bitmap bmp = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Demo.MyPicture.jpg"));
Загрузка из растровых данных и объектов GDIПомимо чтения растров из мест их внешнего хранения, объект Bitmap предоставляет возможность создания "на лету" – из существующих в памяти растров, объектов GDI и поверхностей DirectDraw. Вот соответствующие конструкторы: Bitmap( const BITMAPINFO* gdiBitmapInfo, VOID* gdiBitmapData ); Bitmap( HBITMAP hbm, HPALETTE hpal ); Bitmap( HICON hicon ); Bitmap( IDirectDrawSurface7* surface ); Важно понимать, что все эти конструкторы "однонаправленные" – при их вызове в объекте Bitmap создается собственный буфер, в который копируются (с необходимым преобразованием) растровые данные. При любых модификациях объекта Bitmap (если, например, создать на нем объект Graphics и попробовать в него нарисовать) источник данных остается неизменным – даже после выполнения деструктора Bitmap. Более того, перенос изменений обратно в исходный объект GDI часто сопряжен с трудностями – например, может потребоваться самостоятельная работа с палитрами и битовыми массивами. Некоторые рекомендации по этому поводу можно встретить ниже, в разделе "Работа с растровыми данными напрямую". Отмечу, что, на мой взгляд, в GDI+ было уделено большое внимание обратной совместимости. Вместе с тем, эта совместимость в значительной степени реализована в режиме "только для чтения". Так, например, библиотека корректно прочитает из памяти растр с заголовком BITMAPV4HEADER, содержащим информацию о альфа-канале, но при сохранении в BMP из GDI+ будет сгенерирован только заголовок BITMAPINFOHEADER, и вся информация о прозрачности пропадет.
Существуют также конструкторы, позволяющие задать формат создаваемого растра: Bitmap( INT width, INT height, PixelFormat format ); В GDI+ описано большое количество констант перечисления PixelFormat. По умолчанию это значение равно PixelFormat32bppARGB. Наиболее быстрым для вывода является, однако, формат PixelFormat32bppPARGB, содержащий значения цветовых компонентов, предварительно умноженных в соответствии с величиной прозрачности. Можно самостоятельно выделить буфер для создаваемого растра. Вот соответствующий конструктор: Bitmap( INT width, INT height, INT stride, PixelFormat format, BYTE* scan0 ); Здесь stride – величина смещения (в байтах) между строками растра, scan0 – адрес созданного буфера. Невзирая на формат, величина stride должна делиться нацело на 4 (и естественно, быть достаточно большой для хранения строки пикселов необходимой ширины). Она может также быть отрицательной (для создания растров с обратным порядком следования строк). После уничтожения объекта Bitmap выделенный буфер не удаляется – ответственность за его освобождение лежит на создателе. Взаимодействие растров с объектом GraphicsТекущая реализация библиотеки оперирует 32-битным цветом при любых цветовых вычислениях, даже если один из оперируемых растров имеет индексные (основанные на палитре) цвета. Таким образом, хороший способ экономить ресурсы процессора и память системы – это сразу работать с 32-битными растрами (что и предлагается по умолчанию). Например, при создании экземпляра Graphics из растрового контекста устройства (HDC) создается также промежуточный 32-битный буфер, в который и осуществляется вывод, а уже затем производится копирование этого буфера на контекст. Если контекст устройства имеет другую глубину цвета, то понадобится дополнительное преобразование, что плохо скажется на производительности. Кроме того, такая организация работы с графическими контекстами диктует свои ограничения на обращение к контексту средствами GDI – во время существования Graphics этого делать нельзя. За подробностями обращайтесь к Q311221. Вывод изображений и геометрические преобразованияПоддержка координатных преобразований в GDI+ – слишком большая тема, чтобы касаться ее в разговоре о растровой графике. Однако эта библиотека предоставляет также специальные средства геометрических преобразований при выводе изображений, о которых сейчас пойдет речь. Итак, в объекте Graphics для вывода растров предназначен метод DrawImage. Постойте, я сказал "предназначен метод"? Правильнее будет применить множественное число: их 16 штук! По своему назначению эти перегруженные функции разделяются на 2 основные группы: 1. Вывод изображения или его части в прямоугольную область с возможностью масштабирования по двум осям. По своему действию эти методы похожи на функцию GDI StretchBlt, но с дополнительными возможностями – например, можно получить зеркальное отображение исходного растра. В качестве параметров эти методы получают в различных видах координаты прямоугольных областей источника и приемника: void PaintFlower(Graphics& g, Rect& rc) { if(!flowerImage) { g.DrawString(L"Flower image load error", -1, &font, PointF(float(rc.Width/2), float(rc.Height-20)), &stringFormat, &textBrush); } else { g.DrawImage(flowerImage, flowerPos.X, flowerPos.Y, flowerImage->GetWidth(), flowerImage->GetHeight()); } }
2. Вывод изображения или его части в параллелограмм с аффинными преобразованиями координат всех точек исходного растра. В GDI отсутствует подобная функциональность. Этим методам для работы требуется массив из 3-х точек, образующих вершины параллелограмма (четвертая вершина вычисляется на их основе). Замечу, что частным случаем преобразования при последнем способе является и вращение – достаточно задать 3 координаты прямоугольника, повернутого на требуемый угол. Порядок точек в массиве должен соответствовать точкам A, B и C на приведенной схеме:
Но, конечно, в большинстве случаев для поворота выводимого растра проще будет применить координатные преобразования GDI+. Кроме того, для поворота изображений на угол, кратный 90 градусов, или их отражения относительно осей симметрии, в классе Image существует метод Status RotateFlip( RotateFlipType rotateFlipType ); принимающий в качестве параметра элемент перечисления RotateFlipType. Кстати, все возможные варианты перечислений GDI+ находятся в файле GdiPlusEnums.h.
Для более гибкого контроля над выводом изображения существуют версии метода DrawImage, принимающие указатель на объект класса ImageAttributes. Большая часть задаваемых при этом параметров относится к цветовой коррекции, которая обсуждается ниже. Нас же сейчас может заинтересовать один метод этого класса, влияющий на геометрию выводимых изображений: Status SetWrapMode( WrapMode wrap, const Color& color, BOOL clamp ); Он позволяет указать, каким образом заполнять область вывода за пределами исходной картинки. Вид заполнения определяется параметром wrap из перечисления WrapMode. В частности, указав wrap=WrapModeTile, вы получите вывод "черепицей". При задании режима WrapModeClamp внешняя область заполняется цветом, указанным в параметре color. Параметр clamp в текущей реализации игнорируется. Кстати, все сказанное справедливо не только для растров, но и для вывода объектов Metafile, которые также являются потомками класса Image. Упомянув метод SetWrapMode класса ImageAttributes, нельзя не упомянуть о методах с таким же названием, присутствующих в классах LinearGradientBrush, PathGradientBrush и TextureBrush. Они выполняют аналогичную роль при задании параметров закраски областей. Качество изображенияУвеличивая, растягивая и искажая исходную картинку, можно запросто получить в результате нечто безобразное – зубчатые края изображения, ступенчатые границы прямых линий… Причина кроется в самой природе растровой графики – изображение несет в себе информацию, ограниченную выбранным разрешением. Тем не менее, существуют различные алгоритмы, позволяющие добиться значительного улучшения качества результирующего изображения, или, точнее говоря, качества его человеческого восприятия. Некоторые из них изначально встроены в библиотеку GDI+. Метод Graphics::SetInterpolationMode позволяет указать, какой режим (или алгоритм) интерполяции будет использован при выводе изображения с изменением его пиксельных размеров. Константы возможных режимов описаны в перечислении InterpolationMode: enum InterpolationMode { InterpolationModeInvalid = QualityModeInvalid, InterpolationModeDefault = QualityModeDefault, InterpolationModeLowQuality = QualityModeLow, InterpolationModeHighQuality = QualityModeHigh, InterpolationModeBilinear, InterpolationModeBicubic, InterpolationModeNearestNeighbor, InterpolationModeHighQualityBilinear, InterpolationModeHighQualityBicubic }; Как обычно, в версии .NET имеется соответствующее свойство InterpolationMode в объекте Graphics. Разумеется, выигрывая в качестве, проигрываешь в скорости, поэтому при использовании режима с наивысшим качеством InterpolationModeHighQualityBicubic медленные компьютеры могут выводить изображения больших размеров в течение нескольких секунд! Но только этот метод способен адекватно отображать картинку при уменьшении ее до 25 процентов (и менее) от оригинала. Этот режим очень поможет различным автоматическим генераторам иконок (thumbnail) изображений в ASP.NET. На качество (и скорость) вывода растров также влияют некоторые другие установки класса Graphics. Перечислим их с кратким описанием:
Для получения значений соответствующих настроек служат аналогичные методы с префиксом "Get". В среде Microsoft .NET Framework у класса Graphics существуют аналогичные свойства без префиксов вообще (SmoothingMode, PixelOffsetMode и т.д). Устранение мерцанияЧасто встречающейся проблемой при создании динамично меняющейся графики (и, в частности, анимации) является мерцание. Для его устранения традиционно использовался закадровый буфер, и эта возможность также имеется в GDI+. Объект Graphics можно создать "теневым", используя в качестве основы готовый экземпляр Bitmap (вообще говоря, Image, но нас сейчас метафайлы не интересуют): Graphics( Image* image ); static Graphics* FromImage( Image* image ); При этом все операции вывода с участием такого объекта отразятся на содержимом используемого экземпляра Bitmap. Это предоставляет очень простую возможность двойной буферизации вывода – когда изображение сначала готовится "за кадром", а затем мгновенно (ну, почти мгновенно) переносится на экран, устраняя досадное мерцание при анимации: Вывод с двойной буферизацией в GDI+ (фрагмент демонстрационного приложения) void OnPaint(HDC hdc, RECT& rc) { Graphics g(hdc); Rect paintRect(0, 0, rc.right, rc.bottom); // Создаем временный буфер Bitmap backBuffer(rc.right, rc.bottom, &g); Graphics temp(&backBuffer); // Рисуем в буфер PaintBackground(temp, paintRect); PaintFlower(temp, paintRect); PaintBatterfly(temp, paintRect); // Переносим на экран g.DrawImage(&backBuffer, 0, 0, 0, 0, rc.right, rc.bottom, UnitPixel); } Для создания "теневого" Graphics подойдет далеко не всякий растр – в частности, создание провалится для индексных (работа с палитрами при выводе не поддерживается) и Grayscale-растров (состоящих из оттенков серого цвета). Это связано именно с ограничениями ядра GDI+.
Что касается WinForms, то там режим двойной буферизации уже предусмотрен при рисовании. Для его включения необходимо в отображаемом контроле (или форме) установить флаги UserPaint, AllPaintingInWmPaint и DoubleBuffer перечисления System.Windows.Forms.ControlStyles: . . . protected override void OnLoad(EventArgs e) { SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.DoubleBuffer, true); . . . // другая инициализация base.OnLoad(e); } При этом, кстати, вывод довольно заметно ускоряется (несмотря на необходимость дополнительного переноса на экран) – у меня демонстрационное приложение вместо 70 FPS стало выдавать 75-80. Еще несколько слов о производительностиРаз уж речь зашла о скорости, думаю, уместно будет сказать, что наиболее быстро GDI+ умеет выводить растры оптимизированного для устройства формата, представленные классом CachedBitmap. При их создании необходимо указать оригинальный растр и устройство, на которое будет происходить вывод изображения: CachedBitmap( Bitmap* bitmap, Graphics* graphics ); На этот конструктор накладываются определенные ограничения. В частности, устройство вывода Graphics не должно быть связано с HDC принтера или метафайла. Далее, при смене характеристик устройства вывода (например, при изменении разрешения или глубины цвета экрана) CachedBitmap необходимо пересоздавать для работы с новым устройством, в противном случае вывод производиться не будет. Для вывода оптимизированных растров на экран служит метод Graphics::DrawCachedBitmap: Status DrawCachedBitmap( CachedBitmap* cb, INT x, INT y ); Хорошая новость: DrawCachedBitmap поддерживает прозрачность и альфа-канал (проверено в лабораторных условиях). Плохая новость: применение координатных преобразований к устройству вывода не поддерживается (кроме координатного переноса). В частности, поворачивать такие растры при выводе, к сожалению, нельзя. Если это необходимо, примените технику промежуточного вывода в память с поворотом, а затем уже кэшируйте полученный Bitmap. Разумеется, этот прием не подойдет, если угол поворота меняется динамически (тогда лучше совсем отказаться от кэширования, чтобы не тратить каждый раз время на создание промежуточного изображения).
Пример кэширования многокадрового изображения формата GIF // необходимые переменные Bitmap *batterflyImage; // анимированное изображение бабочки CachedBitmap **cachedFrames; // здесь будут размещаться кэшированные кадры int frameCount; int activeFrame; . . . // инициализация frameCount = batterflyImage->GetFrameCount(&FrameDimensionTime); cachedFrames = new CachedBitmap*[frameCount]; Graphics g(GetWindowDC(0)); // считаем, что текущий видеорежим не изменится // собственно кэширование for(int i=0;i<frameCount;i++) { batterflyImage->SelectActiveFrame(&FrameDimensionTime, i); cachedFrames[i] = new CachedBitmap(batterflyImage, &g); } activeFrame=0; batterflyImage->SelectActiveFrame(&FrameDimensionTime, 0); Перед тем, как использовать CachedBitmap, подумайте о возможных неудобствах: вам придется отлавливать момент изменения видеорежима и соответственно перестраивать все оптимизированные растры. Кроме того, выгода от их применения невелика: у меня в разных тестах она не превышала 9% (при избавлении от многих других тормозящих операций). Куда выгоднее оказалось вынести создание промежуточного контекста Graphics из функции OnPaint в код инициализации, хотя и это не панацея: такой буфер придется пересоздавать при изменении размеров окна. И последнее. Ничего, напоминающего технику DrawCachedBitmap, в среде .NET я не нашел. Демонстрационные приложенияВ завершении данного раздела приведу два небольших демонстрационных приложения. Первое написано на WinAPI. Оно иллюстрирует применение многих описанных приемов работы: отложенную загрузку GdiPlus.DLL, загрузку растров формата GIF из ресурсов программы и использование двойной буферизации для устранения мерцания. Кроме того, в исходном коде есть (хотя и упрощенный) пример работы с анимированным изображением формата GIF. Вы также можете использовать это приложение как полигон для собственных испытаний производительности GDI+. В обработчике WM_PAINT главного окна немедленно вызывается функция InvalidateRect, гарантируя приход следующего сообщения WM_PAINT, и вычисляется число кадров, выводимых в секунду.
Демонстрационный
проект (VC++ 5.0 - 7.0) – 37 Кб Второе написано на языке C# и представляет собой пример вывода анимированных файлов GIF в окне WinForms-программы. В нем также реализованы подсчет производительности (с использованием класса System.Timers.Timer) и двойная буферизация.
Исходный
файл (C#) – 700 байт
Работа с растровыми данными напрямуюК сожалению, у библиотеки GDI+ существуют и ограничения. Некоторые из них связаны с дефектами текущей реализации, и, возможно, вскоре исчезнут. Другие же напрямую следуют из новой архитектуры библиотеки, которая попыталась избавиться от "проклятого наследия" кое-каких архаизмов GDI. Так или иначе, зачастую приходится обращаться напрямую к растровым данным, содержащимся внутри этих красивых оберток. В данном разделе мы узнаем, какие возможности для этого предоставляет GDI+. Класс ColorВ 32-битной цветовой модели наконец-то нашлось применение четвертому байту: он больше не является просто мусором для выравнивания, а законно хранит значение Alpha – технически говоря, величину непрозрачности пиксела. Это было учтено при проектировании класса Color – у него появился соответствующий конструктор: Color( BYTE a, // alpha BYTE r, // red BYTE g, // green BYTE b // blue ); Если используется более традиционная форма конструктора Color с тремя цветовыми компонентами, то значение alpha устанавливается равным 255 (полная непрозрачность). Кроме того, в классе Color описан большой набор именованных цветовых констант (например, Color::AliceBlue) – в них значение Alpha также равно 255. Для среды .NET принят немного другой подход: для инициализации структуры Color у нее существует множество статических свойств – значений цвета (например, DeepSkyBlue), а также методов (например, семейство методов FromArgb). При этом величина каждого из четырех цветовых компонентов также не может превышать 8 бит: public static Color FromArgb(int, int, int, int); У структуры Color в .NET дополнительно имеется такие полезные качества, как возможность преобразования цвета в модель HSB (Hue-Saturation-Brightness, Оттенок-Насыщенность-Яркость): public float GetBrightness(); public float GetHue(); public float GetSaturation(); а также реализация стандартного метода ToString, позволяющая получить "читабельную" строку с названием цвета (если это возможно) или с перечислением его ARGB-компонентов (в противном случае): public override string ToString(); Прямой доступ к пикселамДля получения и установки цвета определенной точки растра класс Graphics предоставляет методы GetPixel/SetPixel: Status GetPixel( INT x, INT y, Color* color ); Status SetPixel( INT x, INT y, const Color& color ); Что можно о них сказать? Не используйте их, если это возможно – их производительность ужасна (как, впрочем, и производительность аналогичных функций GDI). Более быстрым способом будет получение доступа сразу к некоторой прямоугольной области растра. Для этого необходимо использовать метод Bitmap::LockBits: Status LockBits( IN const Rect* rect, // область растра для доступа IN UINT flags, // параметры доступа (чтение, запись) IN PixelFormat format, // константа перечисления PixelFormat OUT BitmapData* lockedBitmapData // место для выходных данных ); Параметр flags формируется из констант перечисления ImageLockMode. Помимо вида доступа к растру, он может содержать флаг ImageLockModeUserInputBuf, указывающий на то, что поле lockedBitmapData->Scan0 уже содержит указатель на выделенный пользователем буфер достаточного размера. Подробное описание этой функции и примеры ее использования можно найти в Platform SDK Documentation. В среде .NET эта функция также реализована, хотя для доступа к растровым данным вам придется применять unsafe-код.
Для доступа к растру исходного изображения можно также воспользоваться следующим (недокументированным) обстоятельством. В GDI+ версии 1 метод Bitmap::GetHBitmap всегда возвращает DIB Section. Вам достаточно вызвать GetObject() для этого HBITMAP, чтобы получить растровые данные и необходимые структуры BITMAPINFO (пример работы с DIB Sections см. в Q186221). Однако на это поведение нельзя полагаться в будущих версиях библиотеки. Поддержка прозрачностиКак мы уже знаем, внутренним форматом библиотеки является 32-битный с альфа-каналом. Из этого факта следует, что растры с прозрачными областями прекрасно поддерживаются: достаточно задать 0 в поле Alpha, и пиксел будет рассматриваться как полностью прозрачный. Именно так загрузчик GIF преобразует растры в 32-битный формат. Если возникла необходимость назначить прозрачным определенный цвет растра, сделать это можно несколькими способами. Во-первых, используя прямую замену цветов при помощи функций GetPixel/SetPixel. При этом придется пройтись по всем точкам картинки, заменяя точки выбранного цвета на прозрачные. Как уже говорилось, этот способ не является быстрым. Во-вторых, можно применить прямой доступ к памяти посредством вызова LockBits. Для .NET необязательно водиться с заменой цветов и "сырыми" растрами, возвращаемыми вызовом LockBits. В классе System.Drawing.Bitmap реализован метод MakeTransparent, который делает прозрачным все точки растра, имеющие выбранный цвет: Bitmap bm=new Bitmap("test.bmp"); // примем цвет первой точки растра за прозрачный Color backColor = bm.GetPixel(0, 0); bm.MakeTransparent(backColor); А что если исходную картинку модифицировать нельзя? Вы, конечно, можете воспользоваться клонированием растра или его фрагмента (используя метод Bitmap::Clone) и совершить замену над точками дубликата. Однако, в GDI+ есть более простой метод, поддерживающий прозрачность только при выводе картинок. Для этого необходимо создать экземпляр класса ImageAttributes, с которым мы уже столкнулись при обсуждении методов Graphics::DrawImage. Этот класс позволяет корректировать многие цветовые параметры выводимого изображения. В частности, метод SetColorKey позволяет указать, что определенный диапазон цветов (вернее, все пикселы, компоненты цвета которых лежат в этом диапазоне) будет заменяться при выводе прозрачным цветом. Посмотрите, например, что станет с бабочкой из демонстрационного проекта, если так модифицировать фрагмент метода PaintBatterfly: ImageAttributes attr; // все цвета в диапазоне (0,0,0,0) – (100,100,100,100) станут прозрачными attr.SetColorKey(Color(0, 0, 0, 0), Color(100, 100, 100, 100)); Rect destRect(batterflyPos, Size(batterflyImage->GetWidth(), batterflyImage->GetHeight())); g.DrawImage(batterflyImage, destRect, 0, 0, batterflyImage->GetWidth(), batterflyImage->GetHeight(), UnitPixel, &attr);
Осталось вспомнить про сохранение растровых изображений с прозрачностью. Если выбранный формат поддерживает альфа-канал (например, PNG), то проблем не возникает. При сохранении же растров популярного в WWW формата GIF с прозрачностью вначале придется вручную преобразовать растр в индексный (основанный на палитре) формат. Рекомендации можно прочесть в уже упомянутой статье HOWTO: Save a .gif File with a New Color Table By Using GDI+ (Q315780). Растровые операцииВ Usenet часто задается вопрос: а как можно заставить GDI+ использовать растровые операции (ROP), определенные в GDI? Например, для выделения набора объектов было бы неплохо их инвертировать (или закрасить инверсным прямоугольником). В САПР может понадобиться рисовать инверсную фигуру или контур (макет) будущего объекта. Кроме того, режим R2_XOR позволяет очень просто восстановить изображение под объектом, всего лишь повторно нарисовав объект на том же месте. Специалисты Microsoft обычно отвечают в духе Дзен: "На самом деле, Вам не нужна такая возможность. XOR-графика попросту уродлива (согласен! – В.Б.). Современные 32-битные графические видеорежимы позволяют выделять и накладывать изображения с помощью Alpha-канала. Применение различных кодов ROP для достижения прозрачности также устарело – прозрачность изначально реализована в GDI+". И действительно, в GDI+ вообще не поддерживаются ROP. Если попробовать "силой" выставить контексту устройства, например, режим R2_XOR, он будет проигнорирован при выводе. Ну что ж, у программистов на C++ еще остается старушка GDI – только не забывайте про уже упомянутые проблемы взаимодействия, описанные в Q311221. А как быть работающим в среде .NET? Оказывается, так же: для .NET существует класс System.Windows.Forms.ControlPaint, не входящий в иерархию GDI+. Его методы фактически обращаются к соответствующим низкоуровневым средствам GDI32.DLL. Для рисования инверсных линий и прямоугольников он предоставляет методы DrawReversibleLine, DrawReversibleFrame и FillReversibleRectangle. Быстрый взгляд в недра последнего подтверждает догадку (без GDI, как видно, иногда не обойтись): . . . IL_002f: ldarg.1 IL_0030: call int32 [System.Drawing]System.Drawing.ColorTranslator::ToWin32( valuetype [System.Drawing]System.Drawing.Color) IL_0035: call native int System.Windows.Forms.SafeNativeMethods::CreateSolidBrush(int32) IL_003a: stloc.3 IL_003b: ldloc.2 IL_003c: ldloc.1 IL_003d: call int32 System.Windows.Forms.SafeNativeMethods::SetROP2(native int, int32) . . . Как использовать эти методы? Вот ссылки на статьи Knowledge Base c соответствующими примерами: HOW
TO: Draw a Rubber Band Rectangle or Focus Rectangle in Visual C# (Q314945) Не зная о существовании этих статей, я самостоятельно набрел на эту возможность и написал пример на C#, позволяющий инвертировать часть изображения. Попробуйте запустить пример, нажать где-нибудь на свободном участке формы левую кнопку мыши и потащить курсор. Выделенный участок инвертируется даже за пределами формы – взаимодействие высокоуровневой GDI+ и низкоуровневой GDI налицо!
Исходный
файл (C#) - 800 байт Думаю, на этом пора остановиться. Следующая часть статьи будет целиком посвящена средствам работы с графическими форматами в GDI+.
Это все на сегодня. Пока! Алекс Jenter jenter@rsdn.ru |
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||