ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 76 от 20 мая 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие подписчики!


 CТАТЬЯ

GDI+
Часть 2. Работа с растровыми изображениями

Необходимые замечания к предыдущей части

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

ПРИМЕЧАНИЕ

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

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

ПРИМЕЧАНИЕ

Должен признаться, что использование в проекте опций отложенной загрузки GdiPlus.Dll иногда приводит к каким-то проблемам в Debug-конфигурации. При попытке собрать приложение с отладочной информацией компоновщик с упрямством выдавал фатальную ошибку. Release- же конфигурация собиралась нормально. Поэтому я отказался от использования delayed loading в отладочных проектах, используя условную компиляцию.

Поддержка 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.

Что нового?

За время, прошедшее с выхода первой части, произошли некоторые изменения.

  • Прежде всего, Visual Studio.NET приобрела статус Release. В связи с этим, .NET Framework SDK также обновился до версии 1.0. Скачать его с сайта Microsoft можно по этому адресу (131 Mb). Для нас это важно, так как большинство примеров работы с GDI+ будет сопровождаться их эквивалентами для .NET Framework.
  • Выпущена в свет новая версия Platform SDK - November 2001. Теперь можно обрадовать многих читателей, не имеющих выделенного соединения с Интернетом: Full Platform SDK включен в состав RSDN CD #0 – приложения на компакт-диске к журналу RSDN Magazine. К сожалению, API GDI+ с тех пор не обновлялся и в составе ноябрьского PSDK по-прежнему находится GDI+ версии 1.0.
  • Обновилась до версии 5.1.3097 библиотека исполнения GDI+ GdiPlus.dll. Возможно (но не обязательно), в ней исправлены некоторые мелкие недочеты, имеющиеся в предыдущей версии (билд 5.1.3092). Для того, чтобы скачать ее, посетите веб-узел Microsoft по адресу http://www.microsoft.com/downloads/release.asp?releaseid=32738
  • Появилась любопытная ссылка на первую реализацию GDI+ для Delphi: http://pages.infinit.net/smarth/gdiplus.html.
  • Вышел в свет первый основанный на GDI+ полномасштабный графический продукт – Visio 2002.
  • Наконец-то появилась долгожданная статья в Knowledge Base HOWTO: Save a .gif File with a New Color Table By Using GDI+ (Q315780). Теперь сохранение прозрачных файлов GIF средствами GDI+ больше не будет тайной!

Похоже, введение затянулось. Ну что же, перейдем, наконец, к теме этой части.

Класс 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). Сейчас единственный разумный способ избежать такого исхода – это убедиться в существовании файла перед его открытием.

ПРЕДУПРЕЖДЕНИЕ

Недавно найдены еще одни "грабли", связанные с тем, что некоторые цифровые фотокамеры не записывают в сохраняемый фотоснимок TIFF его физические размеры. Такой файл прекрасно прочитается кодеком TIFF, и, при везении, даже нарисуется – если явно задать пиксельные размеры получаемого рисунка. В противном случае, GDI+ услужливо попытается рассчитать их на основании физических размеров – и работа приложения завершится с такой же "диагностикой". Будьте бдительны!

Более гибкие возможности загрузки таятся в таком конструкторе 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;
}

ПРИМЕЧАНИЕ

У меня есть серьезное подозрение, что вызов Release в вышеприведенном примере не приведет к немедленному уничтожению потока IStream. Похоже, при загрузке из файлов и потоков объекты Bitmap удерживают источник данных в течение своей жизни и "отпускают" его только в деструкторе. Если об этом забыть, потом можно долго удивляться, почему к графическому файлу нет доступа после прочитывания его в Bitmap.

При загрузке графических ресурсов программы, написанной для .NET, можно также использовать загрузку из объекта System.IO.Stream. Получить экземпляр объекта Stream из ресурса позволяет следующий метод класса System.Reflection.Assembly:


public virtual Stream GetManifestResourceStream(
   string name
);

В таком случае загрузка изображения из ресурсов будет выглядеть примерно так:


Bitmap bmp = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Demo.MyPicture.jpg"));

ПРИМЕЧАНИЕ

Обратите внимание на то, что для доступа к произвольным ресурсам .NET-программы по имени необходимо предварять имя префиксом пространства имен ("Demo" в данном случае).

Загрузка из растровых данных и объектов 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, и вся информация о прозрачности пропадет.

ПРИМЕЧАНИЕ

Не стоит смешивать функциональность кодеков GDI+ и возможности работы с JPEG, которые появились в Windows 98/2000 – они полностью независимы друг от друга. Это поначалу может привести к путанице. В частности, если попытаться создать объект Bitmap из структуры BITMAPINFO, в которой поле bmiHeader.biCompression содержит значение BI_JPEG, такая попытка провалится – потому, что загрузчик из растровых данных GDI+ поддерживает только простые форматы (вроде BI_RGB и BI_BITFIELDS). Для загрузки такого растра просто создайте IStream на блоке данных JPEG и вызовите надлежащий конструктор класса Bitmap.

Что же касается сохранения растров с прозрачностью, к вашим услугам форматы TIFF, PNG и GIF (об этом речь пойдет в Части 3).

Существуют также конструкторы, позволяющие задать формат создаваемого растра:


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());
    }
}

ПРИМЕЧАНИЕ

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

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

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

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

Замечу, что частным случаем преобразования при последнем способе является и вращение – достаточно задать 3 координаты прямоугольника, повернутого на требуемый угол. Порядок точек в массиве должен соответствовать точкам A, B и C на приведенной схеме:


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

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


Status RotateFlip( RotateFlipType rotateFlipType );

принимающий в качестве параметра элемент перечисления RotateFlipType. Кстати, все возможные варианты перечислений GDI+ находятся в файле GdiPlusEnums.h.

ПРИМЕЧАНИЕ

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

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

Метод Назначение
SetSmoothingMode Позволяет указать метод устранения ступенчатости (antialiasing) при выводе примитивов – линий и геометрических фигур
SetCompositingMode Устанавливает или отключает учет прозрачности при наложении растровых изображений
SetCompositingQuality Управляет качеством расчета цветовых компонентов при наложении растров
SetPixelOffsetMode Задает метод учета смещения пикселов при интерполяции. Грубо говоря, определяет, являются ли координаты пикселов (или их центров) целыми числами при расчетах
SetRenderingOrigin Устанавливает позицию начальной точки при смешении (dithering) цветов в 8- и 16-битных режимах

Для получения значений соответствующих настроек служат аналогичные методы с префиксом "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+.

СОВЕТ

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

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

СОВЕТ

Как быть с кэшированием анимированных (многокадровых) изображений? Очень просто: вам потребуется ровно столько объектов CachedBitmap, сколько кадров содержится в анимации. А для того, чтобы кэшировать очередной кадр, необходимо сделать его активным (как и перед выводом его на экран) с помощью метода Image::SelectActiveFrame. Нижеприведенный код иллюстрирует такую технику. Детали же работы с различными графическими форматами (и, в частности, анимацией) будут обсуждаться в части 3, "Работа с графическими форматами".

Пример кэширования многокадрового изображения формата 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 Кб
Откомпилированное приложение (требуется GDI+) – 43 Кб

Второе написано на языке C# и представляет собой пример вывода анимированных файлов GIF в окне WinForms-программы. В нем также реализованы подсчет производительности (с использованием класса System.Timers.Timer) и двойная буферизация.


Исходный файл (C#) – 700 байт
Откомпилированное приложение (требуется .NET Runtime) – 16 Кб

ПРИМЕЧАНИЕ

В обоих примерах вывод анимации намеренно происходит с максимально возможной скоростью, для наглядного показа производительности. В реальном же приложении потребуется получить параметры задержки кадров из графического файла (вызвав функцию Image::GetPropertyItem с параметром PropertyTagFrameDelay). Кроме того, в .NET есть специальный вспомогательный класс System.Drawing.ImageAnimator, который облегчает задачу анимации GIF.

Работа с растровыми данными напрямую

К сожалению, у библиотеки 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-код.

ПРИМЕЧАНИЕ

На самом деле вызов LockBits приводит к копированию указанной области во временный буфер. Изменение растровых данных в этом буфере отразится на содержимом Bitmap только после обратного вызова UnlockBits с тем же указателем lockedBitmapData в качестве параметра.

При этом если указать формат временного буфера (PixelFormat), отличный от формата исходного растра, вызовы LockBits/UnlockBits потребуют дополнительных преобразований.

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

ПРИМЕЧАНИЕ

Если вы действительно вставите данный кусок в демонстрационную программу, то заметите досадную ошибку в текущей версии GDI+. При передаче в метод Graphics::DrawImage ненулевого указателя на класс ImageAttributes начинает выводиться только первый кадр анимации, несмотря на то, что активным кадром может быть любой другой.

Осталось вспомнить про сохранение растровых изображений с прозрачностью. Если выбранный формат поддерживает альфа-канал (например, 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)
HOW TO: Draw a Rubber Band or Focus Rectangle in Visual Basic .NET (Q317479)

Не зная о существовании этих статей, я самостоятельно набрел на эту возможность и написал пример на C#, позволяющий инвертировать часть изображения. Попробуйте запустить пример, нажать где-нибудь на свободном участке формы левую кнопку мыши и потащить курсор. Выделенный участок инвертируется даже за пределами формы – взаимодействие высокоуровневой GDI+ и низкоуровневой GDI налицо!


Исходный файл (C#) - 800 байт
Откомпилированная программа (требуется .NET Runtime) – 2Кб

Думаю, на этом пора остановиться. Следующая часть статьи будет целиком посвящена средствам работы с графическими форматами в GDI+.


Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки