2. Рисование графических примитивов с помощью функций gdi
2.1 Рисование отрезков и кривых
Основные (хотя и не все) функции-члены CDC, предназначенные для рисования
отрезков и кривых, приведены в таблице 5.5.
Таблица 5.5. Функции-члены CDC для рисования отрезков и кривых
Функция
MoveTo
Задает текущую позицию
Назначение
LineTo
Polyline
PolylineTo
Arc
ArcTo
PolyBezier
Рисует отрезок из текущей позиции в заданную точку и смещает в нее
текущую позицию
Последовательно соединяет набор точек отрезками
Соединяет набор точек отрезками прямых, начиная с текущей позиции.
Текущая позиция смещается в последнюю точку набора.
Рисует дугу
Рисует дугу и смещает текущую позицию в конец дуги
Рисует один или несколько сплайнов Безье
PolyBezierTo Рисует один или несколько сплайнов Безье и помещает текущую пози-
цию в конец последнего сплайна
PolyDraw
Рисует набор отрезков и сплайнов Безье через набор точек и смещает
текущую позицию в конец последнего отрезка или сплайна
Для рисования отрезка надо поместить текущую позицию в один из концов от-
резка и вызвать LineTo с координатами второго конца:
dc.MoveTo( 0, 0 );
dc.LineTo( 0, 100 );
При выводе нескольких соединяющихся отрезков MoveTo достаточно вызвать
только для одного из концов первого отрезка, например:
dc.MoveTo( 0, 0 );
dc.LineTo( 0, 100 );
dc.LineTo( 100, 100 );
Несколько отрезков можно построить одним вызовом Polyline или
PolylineTo (отличие между ними в том, что PolylineTo пользуется текущей
позицией, а Polyline – нет). Например, квадрат можно нарисовать так:
POINT aPoint[5] = { 0, 0, 0, 100, 100, 100, 100, 0, 0, 0 };
dc.Polyline( aPoint, 5 );
или с помощью PolylineTo:
dc.MoveTo( 0, 0 );
POINT aPoint[4] = { 0, 100, 100, 100, 100, 0, 0, 0 };
dc.PolylineTo( aPoint, 4 );
Для рисования дуг окружностей и эллипсов предназначена функция CDC::Arc.
В качестве параметров ей передаются координаты описывающего эллипс прямо-
угольника и координаты начальной и конечной точек дуги (эти точки задают углы
для вырезания дуги из эллипса, поэтому могут точно на него не попадать). Ниже при-
веден пример для рисования левой верхней четверти эллипса шириной 200 единиц и
высотой 100 единиц:
CRect rect(0, 0, 200, 100);
CPoint point1(0, -500);
CPoint point2(-500, 0);
dc.Arc(rect, point1, point2);
61
Важная особенность всех функций GDI для рисования отрезков и кривых в
том, что последняя точка не рисуется. Т.е. при рисовании отрезка из точки (0, 0) в
точку (100, 100):
dc.MoveTo( 0, 0 );
dc.LineTo( 100, 100 );
пиксел (100, 100) принадлежать отрезку не будет. Если необходимо, чтобы последний
пиксел тоже был закрашен цветом отрезка, это можно сделать с помощью функции
CDC::SetPixel, предназначенной для закраски отдельных пикселов.
2.2 Рисование эллипсов, многоугольников и других фигур
В GDI есть функции для рисования более сложных примитивов, чем отрезки и
кривые. Некоторые из них перечислены в табл. 5.6.
Таблица 5.6. Функции-члены CDC для рисования замкнутых фигур
Функция
Chord
Ellipse
Pie
Polygon
Описание
Замкнутая фигура, образованная пересечением эллипса и отрезка
Эллипс или окружность
Сектор круговой диаграммы
Многоугольник
Rectangle Прямоугольник
RoundRect Прямоугольник с закругленными углами
Функциям GDI, рисующим замкнутые фигуры, передаются координаты описы-
вающего прямоугольника. Например, чтобы функцией Ellipse нарисовать окруж-
ность, надо указать не центр и радиус, а описывающий квадрат, например:
dc.Ellipse( 0, 0, 100, 100 );
Координаты описывающего прямоугольника можно передавать в виде структу-
ры RECT или как объект CRect:
CRect rect( 0, 0, 100, 100 );
dc.Ellipse( rect );
Как и последняя точка отрезка, нижняя строка и правый столбец описывающе-
го прямоугольника не заполняются. Т.е. при вызове CDC::Rectangle:
dc.Rectangle( 0, 0, 8, 4 );
результат будет такой, как на рис. 5.2.
Рис. 5.2. Прямоугольник, нарисованный вызовом dc.Rectangle(0,0,8,4)
62
2.3 Перья GDI и класс CPen
Для рисования отрезков, кривых и контуров фигур GDI использует объект-
перо, выбранное в контексте устройства. По умолчанию перо рисует сплошную чер-
ную линию толщиной 1 пиксел. Изменить вид линий можно, если создать соответст-
вующий объект-перо и выбрать его в контексте устройства функцией
CDC::SelectObject.
В MFC перья GDI представляются в виде объектов класса CPen. Проще всего
указать характеристика пера в конструкторе CPen, например:
CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );
Второй способ: создать неинициализированный объект CPen, а затем создать
перо GDI вызовом CPen::CreatePen:
CPen pen;
pen.CreatePen( PS_SOLID, 1, RGB(255, 0, 0) );
Третий способ: создать неинициализированный объект CPen, заполнить струк-
туру LOGPEN характеристиками пера, а затем вызвать CPen::CreatePenIndirect
для создания пера GDI:
CPen pen;
LOGPEN lp;
lp.lopnStyle = PS_SOLID;
lp.lopnWidth.x = 1;
lp.lopnColor = RGB(255, 0, 0);
pen.CreatePenIndirect(&lp);
В структуре LOGPEN поле lopnWidth имеет тип POINT, но координата y не ис-
пользуется, а x задает толщину пера.
Функции CreatePen и CreatePenIndirect возвращают TRUE, если перо бы-
ло успешно создано (FALSE – если перо создать не удалось).
У пера есть три параметра: стиль, толщина и цвет. Возможные стили показаны
на рис. 5.3.
PS_DASHDOT
PS_SOLID
PS_DASH
PS_DOT
PS_DASHDOTDOT
PS_NULL
PS_INSIDEFRAME
Рис. 5.3. Стили пера.
Стиль PS_INSIDEFRAME предназначен для рисования линий, которые всегда
располагаются внутри описывающего прямоугольника фигуры. Допустим, вы рисуе-
те окружность диаметром 100 единиц пером PS_SOLID толщиной 20 единиц. Тогда
реальный диаметр окружности по внешней границе будет 120 единиц (см. рис.5.4).
Если ту же окружность нарисовать пером стиля PS_INSIDEFRAME, то диаметр ок-
ружности будет действительно 100 единиц. На рисование отрезков и других прими-
тивов, не имеющих описывающего прямоугольника, стиль PS_INSIDEFRAME не влия-
ет.
63
Рис. 5.4. Стиль пера PS_INSIDEFRAME.
Стиль PS_NULL бывает нужен для рисования фигур без контура (например, эл-
липсов), только с заполнением внутренней области.
Толщина пера задается в логических единицах. Перья стилей PS_DASH,
PS_DOT, PS_DASHDOT и PS_DASHDOTDOT должны быть обязательно толщиной 1 еди-
ница. Если задать толщину 0 единиц, то будет создано перо шириной 1 пиксел неза-
висимо от режима преобразования координат.
Чтобы использовать новое перо, его надо выбрать в контексте устройства. На-
пример, чтобы нарисовать эллипс красным пером толщиной 10 единиц, можно вы-
полнить следующие действия:
CPen pen( PS_SOLID, 10, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject( &pen );
dc.Ellipse( 0, 0, 100, 100 );
2.4 Кисти GDI и класс CBrush
По умолчанию внутренняя область замкнутых фигур (Rectangle, Ellipse и
т.п.) заполняется белыми пикселами. Цвет и стиль заливки определяется параметрами
кисти, выбранной в контексте устройства.
В MFC кисть представляется классом CBrush. Бывают три типа кистей: сплош-
ные, штриховые и шаблонные. Сплошные кисти рисуют одним цветом. Для штрихо-
вых кистей есть 6 предопределенных стилей, они чаще всего используются в инже-
нерных и архитектурных чертежах (рис. 5.5). Шаблонные кисти рисуют путем повто-
рения небольшой битовой карты. У класса CBrush есть конструкторы для создания
кистей каждого типа.
Рис. 5.5. Стили штриховых кистей.
Для создания сплошной кисти в конструкторе CBrush достаточно указать зна-
чение цвета:
64
CBrush brush( RGB(255, 0, 0) );
или создать кисть в два этапа (сначала объект MFC, затем объект GDI):
CBrush brush;
brush.CreateSolidBrush( RGB(255, 0, 0) );
При создании штриховых кистей в конструкторе CBrush указываются стиль и
цвет кисти, например:
CBrush brush( HS_DIAGCROSS, RGB(255, 0, 0) );
или:
CBrush brush;
brush.CreateHatchBrush( HS_DIAGCROSS, RGB(255, 0, 0) );
При рисовании штриховой кистью GDI заполняет "пустые" места цветом фона
(по умолчанию белый, его можно изменить функцией CDC::SetBkColor или
включить/выключить заполнение фона режимом OPAQUE или TRANSPARENT с помо-
щью CDC::SetBkMode). Например, заштрихованный квадрат со стороной 100 единиц
можно нарисовать так:
CBrush brush( HS_DIAGCROSS, RGB (255, 255, 255) );
dc.SelectObject( &brush );
dc.SetBkColor( RGB(192, 192, 192) );
dc.Rectangle( 0, 0, 100, 100 );
2.5 Отображение текста
В предыдущей лекции уже упоминался один из способов вывода текста в окно
с помощью функции CDC::DrawText. Ей можно указать прямоугольник, внутри
которого выводить текст, и флаги, указывающие, как именно располагать текст
внутри прямоугольника. Например, для вывода текста в виде одной строки по центру
прямоугольника rect использовался вызов:
dc.DrawText( "Hello, MFC", -1, &rect,
DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER );
Кроме DrawText, в классе CDC есть еще несколько функций для работы с тек-
стом. Некоторые из них приведены в табл. 5.7. Одна из самых часто используемых –
функция TextOut, которая выводит текст подобно DrawText, но принимает в каче-
стве параметров координаты точки начала вывода текста или использует для этого
текущую позицию. Оператор:
dc.TextOut( 0, 0, "Hello, MFC" );
выведет строку "Hello, MFC", начиная с левого верхнего угла окна, связанного с кон-
текстом dc. Функция TabbedTextOut при выводе строки заменяет символы табуля-
ции на пробелы (массив позиций табуляции передается в качестве параметра).
По умолчанию, координаты, переданные в TextOut, TabbedTextOut и
ExtTextOut, считаются левым верхнем углом описывающего прямоугольника для
первого символа строки. Однако интерпретацию координат можно изменить, задав в
контексте устройства свойство выравнивания текста. Для этого используется функ-
ция CDC::SetTextAlign, например, для выравнивания текста по правой границе:
dc.SetTextAlign( TA_RIGHT );
65
Чтобы функция TextOut вместо явно указанных координат пользовалась те-
кущей позицией, надо вызвать SetTextAlign с указанием стиля и установленным
флагом TA_UPDATECP. Тогда TextOut после вывода каждой строки будет изменять
текущую позицию. Так можно вывести несколько строк подряд с сохранением
корректного расстояния между ними.
Таблица 5.7. Функции-члены CDC для вывода текста
Функция
DrawText
TextOut
Описание
Выводит текст с заданным форматированием внутри описывающего
прямоугольника
Выводит символьную строку в текущей или заданной позиции
TabbedTextOut Выводит символьную строку, содержащую табуляции
ExtTextOut Выводит символьную строку с возможным заполнением описывающе-
го прямоугольника фоновым цветом или изменением межсимвольного
расстояния
GetTextExtent Вычисляет ширину строки с учетом параметров текущего шрифта
GetTabbedText
Extent
GetTextMetric
s
Вычисляет ширину строки с табуляциями с учетом текущего шрифта
Возвращает свойства текущего шрифта (высоту символа, среднюю
ширину символа и т.п.)
SetTextAlign Задает параметры выравнивания для функции TextOut и некоторых
других функций вывода
SetTextJustif
ication
Задает дополнительную ширину, необходимую для выравнивания сим-
вольной строки
SetTextColor Задает в контексте устройства цвет вывода текста
SetBkColor
Задает в контексте устройства цвет фона, которым заполняется область
между символами при выводе текста
Функции GetTextMetrics и GetTextExtent предназначены для получения
свойств текущего шрифта, выбранного в контексте устройства. GetTextMetrics
возвращает эти свойства в виде структуры TEXTMETRIC. GetTextExtent (или
GetTabbedTextExtent) вычисляет в логических единицах ширину заданной строки
с учетом текущего шрифта. Пример использования GetTextExtent – вычисление
ширины промежутка между словами, чтобы равномерно распределить текст по за-
данной ширине. Допустим, надо вывести строку в участке шириной nWidth. Для вы-
равнивания строки по обеим границам можно использовать следующие вызовы:
CString string = "Строка с тремя пробелами ";
CSize size = dc.GetTextExtent( string );
dc.SetTextJustification( nWidth - size.cx, 3 );
dc.TextOut( 0, y, string );
Второй параметр SetTextJustification задает число символов-
разделителей в строке. По умолчанию символом-разделителем является пробел. По-
сле вызова SetTextJustification, все последующие вызовы TextOut и других
текстовых функций будут распределять пространство, заданное первым параметром
SetTextJustification', равномерно между всеми символами-разделителями.
2.6 Шрифты GDI и класс CFont
Все текстовые функции-члены CDC пользуются текущим шрифтом, выбранным
в контексте устройства. Шрифт – это набор символов определенного размера (высо-
ты) и начертания, у которых есть общие свойства, например, толщина символа (нор-
66
мальная или жирная). В типографии размер шрифта измеряется в специальных еди-
ницах – пунктах. 1 пункт примерно равен 1/72 дюйма. Высота символа шрифта 12 пт
равна примерно 1/6 дюйма, но в Windows реальная высота несколько зависит от
свойств устройства вывода. Термин "начертание" определяет общий стиль шрифта.
Например, Times New Roman и Courier New являются различными начертаниями.
Шрифт – один из типов объектов модуля GDI. В MFC для работы со шрифтами
есть класс CFont. Сначала надо создать объект этого класса, а затем с помощью од-
ной из его функций-членов CreateFont, CreateFontIndirect, CreatePointFont
или CreatePointFontIndirect создать шрифт в модуле GDI. Функциям
CreateFont и CreateFontIndirect можно задавать размер шрифта в пикселах, а
CreatePointFont и CreatePointFontIndirect – в пунктах. Например, для соз-
дания 12-пунктного экранного шрифта Times New Roman функцией
CreatePointFont надо выполнить вызовы (размер задается в 1/10 пункта):
CFont font;
font.CreatePointFont( 120, "Times New Roman" );
Сделать то же самое с помощью CreateFont несколько сложнее, т.к. требует-
ся узнать, сколько в контексте устройства логических единиц приходится на один
дюйм по вертикали и затем перевести высоту из пунктов в пикселы:
CClientDC dc(this);
int nHeight = -((dc.GetDeviceCaps(LOGPIXELSY)*12)/72);
CFont font;
font.CreateFont( nHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0,
DEFAULT_CHARSET, OUT_CHARACTER_PRECIS, CLIP_CHARACTER_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH ¦ FF_DONTCARE, "Times New Roman" );
Среди множества параметров CreateFont есть толщина, признак курсива и др
свойства. Эти же свойства шрифта можно хранить в специальной структуре LOGFONT
и передавать ее для создания шрифта в CreatePointFontIndirect, например:
LOGFONT lf;
memset( &lf, 0, sizeof(lf) );
lf.lfHeight = 120;
lf.lfWeight = FW_BOLD;
lf.lfItalic = TRUE;
strcpy( lf.lfFaceName, "Times New Roman" );
CFont font;
font.CreatePointFontIndirect( &lf );
Если вы попытаетесь создать шрифт, не установленный в системе, то GDI по-
пробует подобрать наиболее похожий шрифт из установленных. Хотя внутренний
механизм преобразования шрифтов GDI и пытается это сделать, не всегда результаты
получаются хорошими. Но, по крайней мере, текст на экран выводиться будет.
2.7 Стандартные объекты GDI
В Windows есть набор предопределенных часто используемых перьев, кистей,
шрифтов и других объектов GDI, которые не надо создавать, а можно использовать
уже готовые. Они называются стандартными объектами GDI (табл. 5.8). Их можно
выбирать в контексте устройства с помощью функции CDC::SelectStockObject
или присваивать их существующим объектам CPen, CBrush, и др. с помощью
CGdiObject::CreateStockObject. Класс CGdiObject является базовым классом
для CPen, CBrush, CFont и других MFC-классов, представляющих объекты GDI.
67
Таблица 5.8. Часто используемые стандартные объекты GDI
Объект
NULL_PEN
Пустое (прозрачное) перо
Описание
BLACK_PEN
WHITE_PEN
NULL_BRUSH
HOLLOW_BRUSH
BLACK_BRUSH
DKGRAY_BRUSH
GRAY_BRUSH
LTGRAY_BRUSH
WHITE_BRUSH
Черное сплошное перо толщиной 1 пиксел
Белое сплошное перо толщиной 1 пиксел
Пустая (прозрачная) кисть
То же, что NULL_BRUSH
Черная кисть
Темно-серая кисть
Серая кисть
Светло-серая кисть
Белая кисть
ANSI_FIXED_FONT Моноширинный системный шрифт ANSI
ANSI_VAR_FONT
SYSTEM_FONT
Пропорциональный системный шрифт ANSI
Системный шрифт для пунктов меню, элементов управления и т.п.
SYSTEM_FIXED_FONT Моноширинный системный шрифт (для совместимости со стары-
ми версиями Windows)
Допустим, требуется нарисовать светло-серый круг без контура. Это можно
сделать двумя способами, во-первых:
CPen pen( PS_NULL, 0, (RGB (0, 0, 0)) );
dc.SelectObject( &pen );
CBrush brush( RGB(192, 192, 192) );
dc.SelectObject(&brush);
dc.Ellipse( 0, 0, 100, 100 );
Т.к. прозрачное перо и светло-серая кисть есть среди стандартных объектов
GDI, ту же фигуру можно нарисовать так:
dc.SelectStockObject( NULL_PEN );
dc.SelectStockObject( LTGRAY_BRUSH );
dc.Ellipse( 0, 0, 100, 100 );
2.8 Удаление объектов GDI
Перья, кисти и другие объекты GDI занимают не только память программы, но
и служебную память GDI, объем которой ограничен. Поэтому крайне важно удалять
объекты GDI, которые больше не нужны. При автоматическом создании объектов
CPen, CBrush, CFont и др. подклассов CGdiObject соответствующие объекты GDI
автоматически удаляются из деструкторов этих классов. Если же объекты
CGdiObject создавались динамически оператором new, то обязательно надо вызы-
вать для них оператор delete. Явно удалить объект GDI, не уничтожая объект
CGdiObject, можно вызовом функции CGdiObject::DeleteObject. Стандартные
объекты GDI, даже "созданные" функцией CreateStockObject, удалять не надо.
Visual C++ может автоматически отслеживать объекты GDI, которые вы забы-
ли удалить. Для этого применяется перегрузка оператора new. Чтобы разрешить такое
слежение в конкретном исходном файле, после директивы включения заголовочного
файла Afxwin.h надо добавить директиву определения макроса:
#define new DEBUG_NEW
После завершения работы приложения номера строк и имена файлов, содер-
жащие не удаленные объекты GDI, будут показаны в отладочном окне Visual C++.
68
Для удаления объектов GDI важно знать, что нельзя удалить объект, который
выбран в контексте устройства. Следующий пример является ошибочным:
void CMainWindow::OnPaint()
{
CPaintDC dc( this );
CBrush brush( RGB(255, 0, 0) );
dc.SelectObject( &brush );
dc.Ellipse( 0, 0, 200, 100 );
}
Ошибка заключается в том, что объект CPaintDC создается раньше CBrush.
Т.к. оба объекта созданы автоматически, и CBrush – вторым, то его деструктор будет
вызван первым. Следовательно, соответствующая кисть GDI будет удаляться до того,
как будет удален объект dc. Эта попытка будет неудачной. Вы можете исправить по-
ложение, если создадите кисть первой. Но везде соблюдать подобное правило в про-
грамме тяжело, и очень утомительно искать такие ошибки.
В GDI нет функции для отмены выбора объекта в контексте, вроде
UnselectObject. Решение заключается в том, чтобы перед удалением объекта
CPaintDC выбрать в нем другие объекты GDI, например, стандартную кисть GDI.
Многие программисты поступают по-другому: при первом выборе в контексте уст-
ройства собственного объекта GDI сохраняют указатель на предыдущий объект, ко-
торый возвращается функцией SelectObject. Затем, перед удалением контекста, в
нем выбираются те объекты, которые были в нем "по умолчанию". Например:
CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject(&pen);
CBrush brush( RGB(0, 0, 255) );
CBrush* pOldBrush = dc.SelectObject( &brush );
dc.SelectObject( pOldPen );
dc.SelectObject( pOldBrush );
Способ с использованием стандартных объектов GDI реализуется так:
CPen pen( PS_SOLID, 1, RGB(255, 0, 0) );
dc.SelectObject( &pen );
CBrush brush( RGB(0, 0, 255) );
dc.SelectObject( &brush );
dc.SelectStockObject( BLACK_PEN );
dc.SelectStockObject( WHITE_BRUSH );
При динамическом создании объектов GDI нельзя забывать про оператор delete:
CPen* pPen = new CPen( PS_SOLID, 1, RGB(255, 0, 0) );
CPen* pOldPen = dc.SelectObject( pPen );
dc.SelectObject( pOldPen );
delete pPen;