Глава 7.
Управление графическим выводом
Мы наконец-то подошли к тому моменту, когда пора показать что-нибудь пользователю — рассматривать стандартное окно Windows, пусть даже и созданное самим, уже не интересно. Начать новую тему, конечно же. следует с изложения общих представлений о том, что и как должно работать.
Система Windows создавалась прежде всего как графическая оболочка (это в последнее время начали говорить о ней как о полноценной операционной системе). Соответственно, главное ее отличие (по крайней мере на первый взгляд) от MS-DOS — это графическое представление информации.
Примечание
У меня нет полной уверенности в том, что надо продолжать сравнивать, как это делается уже много лет, MS-DOS и Windows. Однако такое сопоставление все же более понятно, чем сравнение вывода информации в консольных и полноценных приложениях Windows.
Естественно, что практически все приложения используют экран, чтобы отобразить данные, которыми они управляют. Даже если это не так, то все равно визуальные элементы приложений отображаются на экране самой операционной системой. Windows обеспечивает универсальность представления информации как на экране, так и на других устройствах вывода, например на принтере. Причем, что чрезвычайно важно, используются для этого одни и те же примитивы отображения. Система сама распознает целевое устройство и активизирует соответствующий ему модуль. В связи с тем, что Windows — многозадачная система, к приложениям предъявляется ряд требований, исключающих конфликты при использовании функций вывода. Однако это не значит, что Windows обеспечивает приложения только набором функций вывода на экран или печать — система управляет всем выводом. Правильнее будет сказать, что в качестве первичного устройства вывода приложения используют скорее окно, чем непосредственно экран. Каждое устройство вывода в Windows имеет набор текущих параметров, с использованием которых и происходит собственно вывод. Причем в каждый конкретный момент времени только одному приложению соответствует некоторое логическое устройство вывода, что исключает одновременный доступ к последнему — перед началом процесса вывода приложение изменяет параметры вывода, подстраивая их под себя.
Аппаратно-независимый графический вывод
Как вы уже поняли, одна из главных особенностей Windows — независимость графического вывода от устройства. Программное обеспечение, которое поддерживает независимость, содержится в двух библиотеках динамической компоновки (DLL, Dynamic Link Library). Первая, GDI.DLL, обеспечивает графический интерфейс устройства (Graphics Device Interface), а вторая, зависящая от конкретного устройства, является драйвером этого устройства. Например, если приложение осуществляет вывод в область пользовательского окна на дисплее VGA, это библиотека VGA.DLL, а если на принтер Epson FX-80, то — EPSON9.DLL.
Перед операцией вывода на некоторое устройство приложение должно запросить GDI о загрузке соответствующего драйвера (обычно это происходит автоматически и не требует от программиста дополнительных действий). Как только драйвер загружен, приложение может настроить ряд параметров вывода, таких как цвет линии и ее ширина, тип кисти и ее цвет, шрифт, область отсечения и т. д. Система Windows обеспечивает хранение этих и других параметров в специальной структуре, называемой контекстом устройства.
Контекст устройства — структура, определяющая набор графических объектов и связанных с ними атрибутов и графических режимов, которые, собственно, и воздействуют на вывод. Графические объекты включают карандаши для рисования линий, кисти для закрашивания и заполнения, битовые образы для копирования или прокрутки частей экрана, цветовые палитры для определения набора доступных цветов, области для отсечения и других операций, а также контуры для операций рисования и закрашивания.
Сразу же необходимо подчеркнуть — приложение не имеет прямого доступа к контексту устройства, как это было для большинства структур Win32 API. Настройка параметров осуществляется посредством вызова специальных функций.
Используя контекст устройства, приложение может осуществлять следующий набор операций:
Кроме того, приложение может использовать контекст устройства для определения процесса графического вывода, прерывания длительных графических операций, начатых другим потоком многопоточного приложения, а также инициализировать принтер.
В Windows предусмотрено несколько типов контекстов устройств, к знакомству с которыми мы и переходим.
Примечание
При дальнейшем изложении материала я часто буду пользоваться формулировками типа "Win32 API предоставляет" или "Win32 API обеспечивает". Несмотря на то, что предметом наших исследований является библиотека классов MFC, такие обороты не являются оговоркой— классы MFC, говоря откровенно, не более чем надстройка над Win32 API. Здесь же я должен еще раз оговориться — хотя и надстройка, но настолько профессионально сделанная, что зачастую об этом забываешь.
В Windows определены четыре основных типа контекстов устройств:
Примечание
Когда мы говорим о рисовании на экране, то имеется в виду, конечно же, некоторое окно.
Win32 API предоставляет три типа контекста устройства для экрана: контекст класса, общий и частный контексты. Контекст класса и частные контексты устройства используются в приложениях, выполняющих многочисленные операции рисования. Это, в частности, программы автоматизированного проектирования и настольные издательские системы, т. е. такие приложения, которые самостоятельно и постоянно осуществляют вывод и, соответственно, затрачиваемое на эти операции время критично с точки зрения производительности приложения. Общие контексты устройства используются в приложениях, лишь иногда выполняющих операции рисования, и где время, затрачиваемое на этот процесс, не столь существенно. Приложение получает контекст устройства экрана, вызывая функцию Begin-Paint или GetDC и идентифицируя окно, в которое соответствующий вывод будет производиться. Причем тип контекста устройства зависит от того, как приложение зарегистрировало класс окна. Обычно приложение получает контекст устройства экрана непосредственно перед выполнением операций вывода. После завершения вывода приложение должно обязательно освободить контекст устройства вызовом функцию EndPaint или ReleaseDC, соответственно.
Контекст класса
Контекст класса поддерживается только для совместимости с предыдущими версиями Windows. При создании Wm32-пpилoжeния вместо контекстов класса следует использовать частные контексты.
Общий контекст
Общий контекст устройства — контекст устройства экрана, связанный с компонентом управления окна Win32 API и находящийся в области администратора окна. Приложение получает общий контекст устройства, вызывая функцию GetDC, GetDCEx или BeginPaint. Система Windows инициализирует общие контексты устройства значениями по умолчанию, которые можно менять по мере необходимости при помощи специальных функций. Количество общих контекстов в системе ограничено и, следовательно, после завершения операции вывода необходимо сообщить системе об этом, вызвав функцию ReleaseDC или EndPaint. В результате вызова одной из этих функций те изменения параметров контекста, которые были произведены приложением, будут отменены. Каждый раз параметры необходимо устанавливать именно таким образом.
Частный контекст
Частные контексты устройства отличаются от общих тем, что сохраняют любые изменения для заданных по умолчанию данных даже после вызова функции ReleaseDC или EndPaint. Частные контексты устройства не являются частью области администратора окна и, следовательно, лишь однократно инициализируются значениями по умолчанию. Администратор окна автоматически удаляет частный контекст устройства только после того, как последнее окно класса было разрушено.
Приложение создает частный контекст устройства, определяя стиль CS_OWNDC для класса окна в момент инициализации структуры WNDCLASS с последующей регистрацией класса окна. Для получения частных контекстов окон, имеющих стиль CS_OWNDC, используются те же функции: GetDC, GetDCEx или BeginPaint.
Win32 API предоставляет один и тот же тип контекста устройства как для принтера, который может быть матричным, струйным или лазерным, так и для графопостроителя. Приложение создает контекст устройства принтера, вызывая функцию CreateDC. При этом задаются необходимые параметры, такие как имя драйвера принтера, имя принтера, файла или другого устройства вывода. После завершения печати приложение должно вызвать функцию DeleteDC для удаления созданного контекста. Простое освобождение контекста при помощи функции ReleaseDC недостаточно и недопустимо!
Объект в памяти
Для улучшения характеристик вывода изображений, например, исключения мигания, Win32 API предоставляет возможность приложениям полностью подготавливать изображение в памяти до его вывода на экран дисплея и только затем осуществлять непосредственно вывод. Для этого предназначен контекст устройства памяти, который сохраняет растровые изображения для специфического устройства. Приложение создает контекст устройства памяти, вызывая функцию CreateCompatibleDC. Результатом выполнения этой функции является растровое изображение (или растр), имеющее цветной формат, совместимый с форматом первоначального устройства (отсюда и название функции).
Первоначальное изображение в контексте устройства памяти имеет размер один на один пиксел. Прежде чем приложение сможет начать работу с изображением, оно должно установить битовый массив с соответствующей шириной и высотой в контекст устройства, вызывая функцию SelectObject.
Win32 API поддерживает информационный контекст устройства, используемый для восстановления или получения заданных по умолчанию параметров устройства. Для создания информационного контекста приложение должно вызвать функцию Create1С. Для получения информации об объектах, заданных по умолчанию для интересующего устройства, используются функции GetCurrentObject и GetObject. Использование информационного контекста устройства более эффективно, чем контекстов других типов, т. к. Win32 API работает с информационным контекстом на более низком уровне и не создает структуры, необходимые для их работы.
После завершения работы с информационным контекстом необходимо вызвать функцию DeleteDC для удаления созданного контекста.
Система Windows обеспечивает приложения набором графических объектов, которые позволяют управлять выводом. Для этого применяются:
Система Windows поддерживает пять различных графических режимов, которые позволяют приложениям определять тип смешивания цветов, место и параметры вывода и т. д.
Режим настройки фона. Определяет, как происходит смешивание цветов фона текстовых объектов и растровых изображений и фона поля вывода (окна или экрана).
Режим отображения (рисования). Определяет, как происходит смешивание цвета карандашей (pens), кистей (brushes), текстовых объектов и растровых изображений с цветом фона поля вывода (окна).
Режим масштабирования. Определяет преобразования логических координат при графическом выводе в окна или на принтер.
Режим заливки контуров. Определяет использование шаблонов кистей при заливке сложных контуров.
Режим сжатия. Определяет, каким образом происходит преобразование цветов растровых изображений при их увеличении (уменьшении).
Как и в случае графических объектов, контекст устройства инициализируется значениями по умолчанию и для графических режимов.
Работа со шрифтами. Шрифты TrueType
Несмотря на то, что шрифты являются графическими объектами, их применение имеет свои существенные особенности. Именно поэтому их следует рассмотреть отдельно. Операционная система Windows может работать со шрифтами следующих трех типов:
Отличие между видами шрифтов заключается в способе хранения параметров начертания символов в специальных шрифтовых файлах. В случае растровых шрифтов каждый символ хранится в виде растра (битового массива). Векторные шрифты хранят для каждого символа относительные координаты концов отрезков, из которых состоит соответствующий символ. Шрифты TrueType содержат информацию о линиях и командах изгиба, а также настроечную информацию (hints) для точного отображения символа, которая используется при уменьшении или увеличении масштаба отображения.
Растровые шрифты имеют заданные размеры и, следовательно, качество их отображения зависит от устройства вывода. Напротив, векторные шрифты независимы от устройства вывода, однако необходимое для их отображения время больше, чем для растровых или шрифтов TrueType. Последние же обеспечивают приемлемую скорость вывода и могут быть промасштабированы с сохранением изначального вида символов.
Система Windows обеспечивает разработчиков широким набором функций для шрифтового оформления своих приложений, начиная с того, что каждый контекст устройства имеет шрифт по умолчанию, и заканчивая предоставлением системного (встроенного) диалога для выбора шрифтов, который можно использовать в приложении для предоставления пользователям возможности выбора любого шрифта из имеющихся.
Теперь, когда мы в общих чертах познакомились с графическим выводом, самое время вернуться к библиотеке MFC и рассмотреть те же самые вопросы с этой точки зрения.
Классы графического интерфейса
Ввиду важности вопроса еще раз напомню, что весь вывод в системе Windows реализован так, чтобы унифицировать работу с такими физически различными устройствами, как экран дисплея, принтеры, плоттеры и т. п. Для всех устройств вывода приложение использует одни и те же функции и, в общем случае, может "не задумываться" над тем, куда будет выведена строка текста или какой-либо графический объект. Система сама распознает устройство вывода и активизирует соответствующий драйвер. Такой подход, с одной стороны, обеспечивает универсальность процесса графического вывода, а с другой — позволяет, например, создавать платы графических акселераторов, которые самостоятельно, без использования центрального процессора осуществляют преобразование команд рисования, существенно разгружая тем самым всю систему в целом.
Для реализации такого подхода в Windows предусмотрен специальный объект, называемый контекстом устройства. Именно он хранит необходимую информацию как об устройстве вывода, так и о параметрах, собственно, рисования.
Win32 API имеет в своем составе большое количество функций графического вывода и настройки режимов рисования, однако их использование требует соблюдения определенного набора правил, т. к. в противном случае могут возникнуть ошибки во время выполнения программы, сбои при рисовании, потеря части содержимого окон и постепенное исчерпание системных ресурсов, что повлияет на работу не только самого неправильно созданного приложения, но и других приложений и всей системы в целом. Последняя неприятность сплошь и рядом случалась в 16-разрядных версиях Windows, где не освобожденные в явном виде графические ресурсы не освобождались и после завершения приложения. В 32-разрядных версиях, где учет и распределение ресурсов более строги, такое может произойти в случае, если неверно написанное приложение продолжает использоваться, однако после его завершения, даже аварийного, система (за редким исключением) самостоятельно освобождает захваченные ранее ресурсы. Реализация работы с графикой на языке C++ с использованием классов библиотеки MFC позволяет в большинстве случаев гарантировать правильное использование ресурсов (захват и освобождение) в неявном для разработчика виде. С одной стороны, библиотека предоставляет программисту мощные средства графического вывода, но уже в виде функций соответствующих классов, причем имеются и такие, у которых нет аналогов в Win32 API (но это не означает, что в конечном итоге они обходятся без Win32 API). С другой стороны, библиотечные классы берут на себя большую часть работы по созданию, инициализации и корректному освобождению графических ресурсов, что необходимо для правильной и эффективной реализации графического вывода. Как и во многих других операциях, при выводе графики появилась возможность больше сконцентрироваться на самом процессе рисования, а не на подготовке к нему.
Перед тем как перейти к описанию тех возможностей, которые предоставляет библиотека классов MFC, напомню, что для организации графического вывода приложению необходимо работать не только с контекстами устройств, но и с рядом объектов GDI: карандашами, кистями, шрифтами и т. д. Однако функции рисования связаны именно с контекстом устройства.
Посмотрите внимательно на рис. 7.1, на котором представлены все классы библиотеки, имеющие отношение к контекстам устройств.
Рис. 7.1. Классы контекстов устройств библиотеки MFC
Примечание
Параметр CDC "pDC функции CView::OnDraw на самом деле указывает на объект класса CPaintDC, созданный в функции CView::OnPrepareDC, вызываемой библиотечными функциями MFC.
Примечание
Метафайлы, в отличие от других графических файлов, содержат команды рисования. Вывод содержимого файла состоит в выполнении записанных команд. По умолчанию имена метафайлов имеют расширение .WMF (от Windows MetaFile), а расширенных метафайлов — Enhanced MetaFiles — расширение .EMF.
Примечание
В отличие от класса CpaintDC, для объектов класса CMetaFileDC автоматический вызов функции OnPrepareDC не производится — ответственность за ее вызов лежит на разработчике приложения.
Настройка параметров графического вывода проводится двумя путями. Одни параметры настраиваются при помощи вызова функций Win32 API или функций класса CDC и производных от него, другие — путем замены так называемых графических объектов.
Библиотека MFC предоставляет разработчикам все необходимые классы, которые инкапсулируют соответствующие графические объекты Windows (рис. 7.2). Кроме того, библиотека имеет в своем составе дополнительные классы, не имеющие непосредственного отношения к графике, но значительно облегчающие решение ряда задач (классы CPoint, CSize, CRecf, CRectTracker). Все классы имеют один и тот же базовый класс, который позволяет унифицировать работу с графическими объектами как таковыми (CGdiObject). Вот краткий обзор имеющихся классов.
Рис. 7.2. Классы графических объектов библиотеки MFC
Как и все функции Win32 API, работающие с различными объектами Windows, функции графического вывода используют соответствующие дескрипторы. Функции классов графических объектов и контекстов устройств работают также с дескрипторами. В табл. 7.1 показано соответствие представленных классов библиотеки MFC и типов дескрипторов объектов Windows.
Таблица 7.1. Соответствие дескрипторов объектов Windows и классов MFC
Класс MFC |
Объект Windows |
Тип дескриптора |
||
СРеп |
"карандаш" |
HPEN |
||
CBrush |
"кисть" |
HBRUSH |
||
CFont |
"шрифт" |
HFONT |
||
CBitmap |
"битовый массив" |
HBITMAP |
||
CPalette |
"палитра" |
HPALETTE |
||
CRgn |
"регион" |
HRGN |
Помимо того, что каждый из вышеперечисленных классов имеет конструктор, что вполне естественно, в нем также определена специальная функция (функции) инициализации, имеющая префикс Create.
Создание графического объекта любого класса, например СРеn или СBrush. возможно двумя способами. Первый способ заключается в использовании конструктора как для создания собственно объекта, так и для его инициализации. Второй способ, помимо использования конструктора, только создающего объект класса, дополнительно требует вызова инициализирующей функции.
Очевидно, что второй способ предпочтительнее, т. к. невозможность инициализации, например, в случае исчерпания соответствующих системных ресурсов, легче определить при вызове именно функции инициализации. В противном случае, для гарантии защиты от ошибок, вернее, своевременного их обнаружения, потребуется использовать механизм обработки исключений.
Примечание
Описание механизма обработки исключений выходит за рамки книги. Для тех же читателей, которые заинтересуются этим вопросом, могу порекомендовать книгу А. Мешкова и Ю. Тихомирова "Visual C++ и MFC. т. 3".
При использовании графического объекта для настройки соответствующих параметров рисования необходимо выполнить следующие действия:
Примечание
Естественно, что при использовании функций Win32 API приложение манипулирует не указателями, а дескрипторами, не имеющими, в частности, деструктора. Освобождение ресурса необходимо проводить в явном виде вызовом соответствующей функции API.
В случае, если один и тот же объект используется приложением достаточно часто, так что его создание и затем удаление существенно сказывается на производительности приложения в целом, возможно создание и сохранение такого объекта на все время его многократного использования. Однако перед завершением работы приложения следует обеспечить его корректное удаление, и, как следствие, освобождение ресурса, т. е. конкретного графического объекта Windows.
В следующем примере иллюстрируется методика создания и использования графических объектов при помощи классов и функций библиотеки MFC.
void CSunView::OnDraw(CDC* pDC)
{
// Создание с одновременной инициализацией
CPen pen_l (PS__DOT, 1, RGB(0, О, О));
try
{
// Создание с одновременной инициализацией
CBrush brush(HS_CROSS, RGB(0, 0, 0));
// Создание
CPen pen_2;
// Инициализация
if (pen_2.CreatePen(PS_SOLID, 2, RGB(192, 192, 192))).
{
// Устанавливаем новый карандаш,
// сохраняя указатель на текущий
CPen* pOldPen = pDC->SeiectObject(&pen_2);
// Рисование с использованием нового карандаша
pDC->Rectangle(...);
// Восстанавливаем старый карандаш
pDC->SelectObject(pOldPen);
}
else (
// Обработка невозможности инициализации реп_2
}
}
catch (CResourceException *e)
{
// Обработка невозможности инициализации brush
}
}
Обратите внимание, что по завершении выполнения функции все созданные объекты классов CBrush и СРеn будут автоматически уничтожены, и их деструкторы выполнят действия, необходимые для освобождения захваченных ресурсов. Кроме того, в примере не проводится какая-либо диагностика возможной ошибки при инициализации объекта реn_1, а для объекта brush выполняется проверка возникновения исключительной ситуации, которая возбуждается при невозможности создания объекта и имеет тип указателя на объект CResourceException. Причем при возникновении исключения сразу же после выполнения конструктора управление будет передано на оператор catch, а промежуточные операторы будут проигнорированы. В этой главе я ничего не сказал о поддержке графических режимов библиотекой MFC, но к этому вопросу мы еще вернемся в соответствующем месте, а теперь пора переходить непосредственно к работе с представленными классами.