GDI+: графика нового поколения

Часть 1. Краткое знакомство

Автор: Виталий Брусенцев
The RSDN Group

Источник: RSDN Magazine #1
Опубликовано: 13.12.2001
Версия текста: 1.0
Заглянем "под капот"
Что новенького?
Требования к среде выполнения
Поддерживаемые технологии разработки
Начинаем работу
Иерархия классов GDI+
Инициализация и завершение
Создаем первое приложение
Пример WinForms - приложения с использованием GDI+
Несколько замечаний о компиляции и сборке проектов

Демонстрационное приложение на C++ (требует наличия GDI+) - 88 Кб.
Демонстрационное приложение на C# (требует CLR) - 62 Кб.

За последний год компания Microsoft подготовила разработчикам множество сюрпризов. Новые продукты и технологии буквально завладели вниманием околокомпьютерного мира. Пока неясно, насколько успешным будет дебют технологии .NET и основанных на ней программных продуктов и средств разработки. Но одно из новшеств, безусловно, уже завоевало признание разработчиков, связанных с графикой и мультимедиа, - технология GDI+. Именно она, вернее, основанный на ней новый графический интерфейс является "лицом" новых операционных систем - Windows XP и .NET Server.

Что же такое GDI+? Официальная документация скромно называет ее Class-based API, то есть основанным на классах интерфейсе прикладных программ. Так как она встроена в Windows XP и .NET Server, ее называют частью этих операционных систем. Часто встречается также определение "библиотека" или "библиотека классов". В действительности, предоставляемый GDI+ набор классов является тонкой оболочкой над множеством обычных функций, реализованных в одной динамической библиотеке GdiPlus.dll. В общем, имея все это в виду, будем для краткости далее называть ее просто библиотекой.

Итак, GDI+ - это библиотека, призванная заменить существующий уже больше 11 (или 18 - как считать) лет интерфейс GDI, являющийся графическим ядром предыдущих версий Windows. Она сочетает в себе (по крайней мере, по замыслу) все достоинства своего предшественника и предоставляет множество новых мощных возможностей. Кроме того, при ее проектировании заранее ставилась цель наименее болезненного переноса приложений на 64-битные платформы. Следовательно, хотя существующие GDI-приложения будут выполняться на новых версиях Windows, для новых проектов следует использовать GDI+.

Заглянем "под капот"

Что новенького?

Далее мы еще будем рассматривать специфические (и такие эффектные!) возможности GDI+. Здесь же только опишем основные новшества.

Достоинства C++ - реализации:

Архитектурные новинки библиотеки:

Новые технологии и возможности (задержите дыхание):

Требования к среде выполнения

Поддержка GDI+ встроена непосредственно в операционные системы Windows XP и .NET Server. Для того чтобы приложения, использующие эту библиотеку, выполнялись на предыдущих версиях Windows, необходимо установить дистрибутив gdiplus_dnld.exe размером около одного мегабайта. Найти его (и, возможно, другие необходимые обновления) можно на сайте Microsoft по адресу:

http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdkredist.htm

В его состав входят только инструкция по установке и уже упомянутая динамическая библиотека GdiPlus.dll, которую необходимо скопировать в системный каталог Windows 98/ME, Windows NT SP6 или Windows 2000. При этом возможности, предоставляемые непосредственно ядром Windows XP (в частности, технология ClearType для качественного отображения шрифтов на LCD-мониторах), будут недоступны.

ПРИМЕЧАНИЕ
Я не случайно не упомянул про Windows 95. На сайте Microsoft отсутствует всяческое упоминание о поддержке GDI+ для этой операционной системы. Тем не менее, единственная доступная мне для тестирования машина с Windows 95 OSR2 выполнила тестовое приложение без каких-либо проблем. Но ввиду отсутствия какой-либо официальной поддержки для использования GDI+ крайне рекомендуется обновить систему хотя бы до Windows 98.

Поддерживаемые технологии разработки

В этой статье рассматривается интерфейс к GDI+, реализованный для языка C++ - хотя уже существует реализация Microsoft для системы CLR, входящей в состав .NET, и, безусловно, вскоре усилиями энтузиастов появятся другие (например, для VB и Delphi).

Заметим, что GDI+ (вернее, ее обертка для CLR), входящая в состав Microsoft .NET Framework SDK, является основным средством рисования в среде .NET. Однако доступная на данный момент Beta 2 имеет довольно большие отличия от реализации для C++ (не только архитектурные, но и чисто внешние, например, различающиеся имена некоторых классов). Я постараюсь коротко описать эти отличия в конце статьи.

Набор заголовочных файлов (headers) и библиотека импорта GdiPlus.lib, необходимые для сборки демонстрационных приложений, входят в состав последнего Platform SDK. Те, кто до сих пор не обновил идущий с Visual Studio 6.0 Platform SDK образца 1998 года, могут загрузить его с сайта Microsoft по адресу:

http://www.microsoft.com/msdownload/platformsdk/sdkupdate/

Минимальный компонент, в состав которого входит GDI+, называется Windows Core SDK и имеет размер около 230 мегабайт.

ПРИМЕЧАНИЕ
Я понимаю, что для многих читателей, имеющих доступ в Интернет через домашний модем, предложение скачать дистрибутив такого размера прозвучит как насмешка. В качестве крайней временной меры можно раздобыть только набор заголовочных файлов GdiPlus*.h, BaseTsd.h и библиотеку импорта GdiPlus.Lib из нового Platform SDK. Но гарантировать работоспособность такого решения во всех ситуациях я не возьмусь. Да и в любом случае, обновить Platform SDK необходимо. Возможно, вам удастся найти его на CD-ROM.

На момент написания этих строк доступна версия Platform SDK за август 2001 г.

Демонстрационные примеры будут в подавляющем большинстве написаны с использованием Windows API, что позволит сосредоточиться на использовании GDI+. Но вы без труда сможете подключить эту библиотеку к своим MFC- или WTL-приложениям. Иногда я также буду приводить соответствующий пример на C# для WinForms.

Начинаем работу

Иерархия классов GDI+

Типичное рабочее место программиста на C++, как правило, включает в себя стену, на которой гордо красуется Диаграмма классов (неважно каких). Теперь рядом можно наклеить еще один плакат.

Ниже приведена иерархия классов GDI+. Я не включил в нее 8 структур данных и перечисления (enumerations) - около 50 штук.


При первом взгляде на диаграмму видно, что она очень напоминает, например, ту часть библиотеки MFC, которая отвечает за рисование, только классов гораздо больше (40 против 15 у MFC). Это и неудивительно, учитывая фирму, которая разрабатывала эти библиотеки. Основные отличия отражают новые возможности GDI+. Мы подробно рассмотрим их в следующих частях.

Как видим, большинство объектов имеют в корне иерархии класс GdiPlusBase. Вам не понадобится создавать экземпляры этого класса, так как он содержит только средства управления памятью (для него перегружены операторы new/new[] и delete/delete[], которые используют функции GDI+ GdipAlloc и GdipFree). Все классы, инкапсулирующие работу с ресурсами GDI+, порождены от GdiPlusBase. Это не значит, что их экземпляры нельзя создавать на стеке - напротив, так даже удобнее контролировать время их жизни. Зато такая архитектура позволит, например, передавать указатель на созданный объект GDI+ в модуль, написанный с использованием других средств разработки, и безопасно его удалять в этом модуле.

Не путайте управление памятью под экземпляры классов-оберток С++, которое осуществляется перегруженными операторами new/delete, и управление собственно ресурсами GDI+, которое скрыто от разработчиков в недрах соответствующих функций, например, GdipCreateSolidFill.

Ключевым же классом в GDI+ является Graphics (программисты на J++ вздрогнули). Именно он содержит почти две сотни методов, отвечающих за рисование, отсечение и параметры устройства вывода. Напрашивается явная аналогия с контекстом устройства (Device Context) прежнего GDI, и эти понятия действительно тесно связаны. Из четырех конструкторов Graphics два создают его из HDC. Главное отличие заключается в изменении программной модели: теперь вы не работаете с хендлом, а вызываете методы класса. Хотя программистам на MFC эта концепция уже хорошо знакома.

Дальнейшее наследование (например, класс TextureBrush порожден от Brush) скорее отражает цели разработчиков (скрытие деталей реализации и повторное использование оберточного кода), чем инфраструктуру библиотеки, так как в inline-методах "родственных" классов просто содержатся вызовы различных функций GdiPlus.dll. Можно сказать, что Microsoft в очередной раз спроецировала обычный "плоский" API языка C на объектно-ориентированную библиотеку C++.

Оставшаяся часть классов не имеет общего родителя и предназначена для упрощения работы со структурами данных GDI+.

Инициализация и завершение

Перед тем как начать использовать классы и функции GDI+, необходимо инициализировать эту библиотеку. Для этого где-нибудь в начале своей программы нужно поместить вызов функции GdiplusStartup:

Status GdiplusStartup( ULONG_PTR* token, 
    const GdiplusStartupInput* input, 
    GdiplusStartupOutput* output );

Поля структуры GdiplusStartupInput управляют различными аспектами инициализации: в частности, можно задать функцию, которая будет вызываться при возникновении ошибок, или перехватывать все обращения к функциям GDI+. Эти детали мы рассматривать не будем. К счастью, конструктор по умолчанию структуры GdiplusStartupInput выполняет инициализацию, достаточную в большинстве случаев. При этом в качестве выходного параметра output можно задать NULL.

"Магическое значение", на которое указывает выходной параметр token, необходимо сохранить.

Для завершения работы с библиотекой вызовите функцию GdiplusShutdown:

VOID GdiplusShutdown(
    ULONG_PTR token
);

Здесь в качестве параметра и необходимо передать то самое число, которое возвратила GdiplusStartup в параметре token.

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

Создаем первое приложение

Настало время применить все эти сведения на практике. Для этого создадим в MS Visual C++ базовое WINAPI-приложение, которое послужит полигоном для дальнейших экспериментов. Ниже для этого приведена пошаговая процедура.

Итак, создаем новый проект Win32 Application. Выбираем опцию A typical "Hello, World!" application и нажимаем "Finish". Получившееся приложение необходимо подготовить для использования GDI+. Для этого в файле stdafx.h после строки с комментарием:

// TODO: reference additional headers your program requires here

добавляем следующие строчки:

#include <GdiPlus.h>
using namespace Gdiplus;

и в конце файла stdafx.cpp добавляем строку

#pragma comment(lib, "GdiPlus.lib")

Кроме того, в файле stdafx.h необходимо удалить или закомментировать строку

#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 

Иначе компилятор выдаст кучу ошибок об отсутствии символов MIDL_INTERFACE, PROPID, IStream и т.д.

Если полученное в результате приложение успешно собралось, значит, мы все сделали правильно. Пойдем дальше.

Найдем в сгенерированном основном .cpp файле нашего проекта функцию WinMain и добавим в начале ее код инициализации:

GdiplusStartupInput gdiplusStartupInput; 
ULONG_PTR gdiplusToken; 
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

а в конце, перед оператором return, добавим код очистки:

GdiplusShutdown(gdiplusToken);

Готово. Наконец-то мы можем что-нибудь нарисовать. Найдите в теле функции WndProc обработчик сообщения WM_PAINT и замените следующим кодом:

hdc = BeginPaint(hWnd, &ps);
OnPaint(hdc, ps.rcPaint);
EndPaint(hWnd, &ps);
return 0;

Теперь где-нибудь перед функцией WndProc создадим функцию OnPaint с кодом рисования:

void OnPaint(HDC hdc, const RECT& rc)
{
    // Все строки - в кодировке Unicode
    WCHAR welcome[]=L"Welcome, GDI+ !";

    // Создаем контекст рисования и устанавливаем 
    // пиксельную систему координат
    Graphics g(hdc);
    g.SetPageUnit(UnitPixel);
    RectF bounds(0, 0, float(rc.right), float(rc.bottom));

    // Загружаем фоновое изображение и растягиваем его на все окно
    Image bg(L"BACKGRND.gif");
    g.DrawImage(&bg, bounds);

    // Создаем кисть с градиентом на все окно и полупрозрачностью
    LinearGradientBrush brush(bounds, Color(130, 255, 0, 0), Color(255, 0, 0, 255), 
            LinearGradientModeBackwardDiagonal); 
    
    // Готовим формат и параметры шрифта
    StringFormat format;
    format.SetAlignment(StringAlignmentCenter);
    format.SetLineAlignment(StringAlignmentCenter);
    Font font(L"Arial", 48, FontStyleBold);

    // Выводим текст приветствия, длина -1 означает, 
    // что строка заканчивается нулем
    g.DrawString(welcome, -1, &font, bounds, &format, &brush);
}

В результате у нас получится примерно вот что:


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

Пример WinForms - приложения с использованием GDI+

Для того чтобы можно было сравнить рассматриваемую реализацию GDI+ с той, что используется в .NET, приведу полный текст соответствующего приложения на новом языке C#:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public class GraphicsForm: Form
{
    public static int Main() 
    {
        Form fm = new GraphicsForm();
        fm.ShowDialog();
        return 0;
    }
    protected override void OnPaint(PaintEventArgs a)
    {
        DoPaint(a.Graphics, a.ClipRectangle);
    }

    protected void DoPaint(Graphics g, Rectangle clipBox)
    {
        RectangleF bounds = clipBox;
        string welcome = "Welcome, GDI+ !";
        Bitmap bg = new Bitmap("BACKGRND.gif");
        g.DrawImage(bg, bounds);
        LinearGradientBrush brush = new LinearGradientBrush(bounds,
            Color.FromArgb(130, 255, 0, 0), Color.FromArgb(255, 0, 0, 255),
            LinearGradientMode.BackwardDiagonal);  
        StringFormat format = new StringFormat();
        format.Alignment = StringAlignment.Center;
        format.LineAlignment = StringAlignment.Center;
        Font font = new Font("Arial", 48, FontStyle.Bold);
        g.DrawString(welcome, font, brush, bounds, format);     
    }
}

Как видим, помимо чисто синтаксических отличий имеются и принципиальные, например, использование в CLR-модели свойств против использования Set-методов в C++. Кроме того, в .NET активно используются пространства имен.

Замечу, что здесь приведен полный текст программы, аналогичной по возможностям той, что мы создали в предыдущем разделе. Сравните объем исходных текстов этих двух примеров. NO COMMENTS.

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

Несколько замечаний о компиляции и сборке проектов

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

Где взять GdiPlus.h?

Как я уже сказал, все заголовочные файлы, библиотека импорта и документация к библиотеке входят в состав последнего Platform SDK. Они не идут в составе Visual С++ 6.0 и его сервис паков.

Почему выдается ошибка о типе ULONG_PTR?

Похоже, что компилятор находит старый заголовочный файл basetsd.h - например, из комплекта VC++. Измените пути поиска заголовочных файлов так, чтобы вначале были найдены файлы Platform SDK.

Почему компилятор не дает создать объект GDI+ при помощи new?

Такое поведение возможно при попытке откомпилировать MFC-приложение с использованием GDI+ в Debug-конфигурации.

В начале файла программы, видимо, имеется следующий фрагмент:

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

Либо откажитесь от создания объектов GDI+ с помощью new, либо откажитесь от проверок динамической памяти в этом файле (удалив вышеприведенную директиву #define).

Не забудьте про пространство имен Gdiplus и библиотеку импорта

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

using namespace Gdiplus;

Если это решение не подходит (например, в проекте уже существуют классы с такими именами), то перед именами классов необходимо ставить префикс пространства имен, например

Gdiplus::Rect rect;

Также, если по каким-то соображениям директива

#pragma comment(lib, "gdiplus.lib")

не устраивает, в опциях компоновщика нужно явно указать библиотеку импорта gdiplus.lib.

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


Эта статья опубликована в журнале RSDN Magazine #1. Информацию о журнале можно найти здесь