Глава 5. Графика в Windows

Общие сведения

hdc

Для рисования в Windows вы должны использовать функции GDI (Graphics Device Interface). Если вы хотите что-то нарисовать на устройстве вывода (экран монитора или принтер), вы должны в самом начале получить описатель контекста устройства (device context), который далее будет называться DC. Полученный описатель передается как параметр большинству функций из GDI, чтобы идентифицировать логическое устройство вывода.

Контекст устройства содержит в себе множество атрибутов, определяющих, как GDI будет работать с этим логическим устройством. Такими атрибутами являются: шрифт, цвет текста, цвет пера, цвет фона текста, кисть и т.д. Чтобы изменить любой из этих атрибутов, вы должны вызвать специальную функцию.

Cуществует множество способов получения контеста устройства, после получения контекста монитора, его необходимо вернуть назад перед выходом из оконной процедуры, то есть контекст дается именно вам на время и он один на всех. Подробнее о взятии и отдаче контекста в сообщении WM_PAINT будет написано ниже. Для получения контекста в произвольном месте программы, вы должны использовать функцию GetDC или GetWindowDC, для возвращения контекста назад – ReleaseDC. Весь блок рисования должен находиться между этими функциями:
hdc := GetDC(hwnd);   //взяли контекст
//функции рисования
ReleaseDC(hwnd, hdc); //вернули назад в систему

или

hdc := GetWindowDC(hwnd);
//функции рисования
ReleaseDC(hwnd, hdc);

Функция GetDC получает контекст клиентской части окна, определяемого hwnd, а функция GetWindowDC получает контекст всего окна, а не только его клентской части. Вы также можете получить контекст всего экрана целиком, если в GetDC задать параметр равным нулю.

Существуют и другие функции получения контекста (CreateDC, CreateCompatibleDC и т.п.), но о них как-нибудь потом.

Цвета

Для задания цвета в GDI используется длинное беззнаковое целое (DWORD), в большинстве случаев цвет хранится в виде RGB (Red Green Blue) составляющих, соединенных в одно число, каждая составляющая имеет тип BYTE, для преобразования цвета существуют функции RGB, GetRValue, GetGValue, GetBValue, назначение этих функций понятно и без пояснений. В файле windows.pas определен тип-синоним для DWORD COLORREF, который служит специально для задания цвета. Вот заголовок функции RGB и GetRValue:

function RGB(r, g, b: Byte): DWORD;
function GetRValue(rgb: DWORD): Byte;

Далее везде где упоминается слово ЦВЕТ, использутся именно этот способ задания и интерпретации цвета.

Для задания фонового цвета служит функция SetBkColor

function SetBkColor(DC: HDC; Color: COLORREF): COLORREF;

Обратите внимание, что эта функция определяет не цвет фона окна, а цвет фона текста и цвет, которым закрашиваются промежутки в пунктирных линиях! Также используется в некоторых других случаях, для подробностей обращайтесь к справке по win32.

Для получения фонового цвета используется соответственно GetBkColor.

Вместо цвета фона можно использовать прозрачный фон, это достигается вызовом функции SetBkMode:
SetBkMode(dc, TRANSPARENT);//фон прозрачный
SetBkMode(dc, OPAQUE);	   //фон заполняется текущим фоновым цветом

Для изменения цвета выводимого цвета используется функция SetTextColor:

function SetTextColor(DC: HDC; Color: COLORREF): COLORREF;

Значение, устанавливаемое этой функцией, используется такими функциями как TextOut и ExtTextOut для в качестве цвета выводимого текста. Как и SetBkColor эта функция возвращает предыдущий цветовое значение. Для получения цвета текста в контексте используется функция GetTextColor.

Многие функции GDI принимают значение цвета как один из параметров.

Перья (pens)

Перо (pen) – это графический инструмент, используемый Windows для рисования линий. Перо характеризуется такими атрибутами как cтиль, цвет, толщина. Здесь будет идти речь только о самых простых типах переьев, для решения более сложных вопросов обращайтесь к соответствующей литературе.

Для создания пера можно использовать функцию CreatePen:

function CreatePen(iStyle, iWidth: Integer; crColor: COLORREF): HPEN;
iStyle
стиль пера, одна из следующих констант:
константы стиля для создания пера
iWidth
толщина пера в логических единицах, если равна нулю, то GDI рисует линию толщиной в один пиксель; для типов пера PS_DASH, PS_DOT, PS_DASHDOT должен быть равен 1 или меньше;
crColor
цвет пера.

В случае успеха возвращается описатель нового пера, в противном случае – нуль. Если в пере больше нет нужды, его уничтожают функцией DeleteObject. Пример создания и уничтожения пера:
var hPen : HPEN;
...
hPen := CreatePen(PS_SOLID, 10, RGB(255, 0, 0));//создание пера красного цвета толщиной 10
...//использование пера для рисования, например
DeleteObject(hPen);

Также для создания пера можно использовать функцию CreatePenIndirect, которая в качестве параметра принимает запись (структуру) с описанием всех свойств нового пера. Чтобы ваше перо стало доступным контексту, его надо в него включить, это делается при помощи функции SelectObject:

function SelectObject(DC: HDC; p2: HGDIOBJ): HGDIOBJ;

Эта функция связывает с графическим контекстом некоторый объект GDI, возвращая при этом описатель старого. Возвращаемое значение всегда нужно сохранять, чтобы после завершения работы с контекстом восстановить его предыдущее состояние. Вот пример для CreatePen:
var hPen, hOldPen : HPEN;
...
hPen := CreatePen(PS_SOLID, 10, RGB(255, 0, 0));
hOldPen := SelectObject(hDC, hPen);//связываем перо с контекстом
...//рисование пером
SelectObject(hDC, hOldPen); //восстанавливаем предыдущее состояние
DeleteObject(hPen);//удаляем перо

Для создания более «продвинутых» перьев служит функция ExtCreatePen, но о ней я уже рассказывать не буду – вы можете сами прочитать описание в справке по win32.

Кисти (brushes)

Кисть (brush) – это графический инструмент GDI, предназначенный для закрашиванию областей (например, внутренности эллипса или прямоугольника). Кисть может быть однотонной (одного цвета), состоять из так называемого узора (pattern), или определяться произвольным битмапом (bitmap). Для программиста кисть определяется своим описателем, получаемым как результат вызова функции создания кисти. Кисть связывается с контекстом аналогично перу при помощи функции SelectObject. Для создания одноцветной кисти используется функция CreateSolidBrush:

function CreateSolidBrush(crColor: COLORREF): HBRUSH;

crColor определяет цвет кисти

Для создания кисти со стандартным узором используется функция CreateHatchBrush:

function CreateHatchBrush(iStyle: Integer; crColor: COLORREF): HBRUSH;
iStyle
определяет стиль кисти, должен быть одной из следующих констант:
константы стиля кисти для функции CreateHatchBrush
crColor
цвет кисти, точнее, цвет линий кисти. Цвет между линиями определяется текущим цветом фона и режимом фона. Цвет фона задается функцией SetBkColor.

Для создания кистей, основанных на битмапах используется функция CreatePatternBrush:

function CreatePatternBrush(hBitmap: HBITMAP): HBRUSH;

hBitmap – это описатель битмапа, на основе которого будет построена кисть, при применении этой кисти вы получите область «залитую» этими битмапами, повторяющимися по горизонтали и вертикали. Также для создания таких кистей можно использовать функцию CreateDIBPatternBrushPt.

Для создания кисти определенного стиля, цвета и узора используется функция CreateBrushIndirect:

function CreateBrushIndirect(const lb: TLogBrush): HBRUSH;
  PLogBrush = ^TLogBrush;
  TLogBrush = packed record
    lbStyle: UINT;
    lbColor: COLORREF;
    lbHatch: Longint;
  end;

В эту функцию передается предварительно заполненная запись типа TLogBrush. Параметры этой записи такие:

lbStyle
определяет стиль кисти, должен быть одной из следующих констант:
BS_DIBPATTERN
узор кисти определяется устройство-независимым битмапом, описатель на область памяти. занимаемой этим битмапом передается через параметр lbHatch;
BS_DIBPATTERN8X8
синоним BS_DIBPATTERN
BS_DIBPATTERNPT
то же самое, что и BS_DIBPATTERN, только в параметре lbHatch передается не описатель области памяти, а непосредственно указатель;
BS_HATCHED
кисть с простым узором, как у CreateHatchedBrush;
BS_HOLLOW
пустая кисть;
BS_NULL
пустая кисть;
BS_PATTERN
узор определяется битмапом, описатель которого передается через параметр lbHatch;
BS_PATTERN8X8
синоним BS_PATTERN;
BS_SOLID
однотонная кисть.
lbColor
определяет цвет кисти, когда lbStyle равен BS_HOLLOW или BS_PATTERN игнорируется.
lbHatch
если lbStyle равно BS_HATCHED, то определяет тип узора через одну из констант

в остальных случаях все объяснено при описании параметра lbStyle.

После создания кисть связывается с контекстом при помощи SelectObject. Для получения информации о кисти используется функция GetObject:
var lb : TLogBrush;
...
GetObject(hBrush, sizeof(TLogBrush), @lb);

После такого вызова переменная lb будет содержать информацию о кисти hBrush. Уничтожается кисть при помощи DeleteObject и только после освобождения из контекста. Пример:
var hBr, hOldBr : HBRUSH;
...
hBr := CreateSolidBrush(RGB(0, 255, 0));//создаем синюю кисть
hOldBr := SelectObject(hDC, hBr);//сохраняем старую кисть
...//рисуем что-нибудь
SelectObject(hDC, hOldBr);
DeleteObject(hBr);//удаляем кисть

Шрифты

Графический контекст содержит в себе графический инструмент шрифт, который используется функциями рисования текста для вывода этого самого текста на логическое устройство. Контекст может содержать только один шрифт. Объект шрифт в GDI хранится во внутренем скрытом формате, и программист должен осуществлять все манипуляции с ним используя описатель (handle). Под фразой «создать шрифт» далее будет подразумеваться создание соответствующего объекта GDI, все манипуляции со шрифтом происходят с полученным при его создании описателем. Стандартные системные шрифты можно не создавать, так как они уже существуют в глубине системы; получить их можно при помощи функции GetStockObject, речь о которой пойдет ниже.

Параметры шрифта

Если говорить формально, то шрифт – это набор символов имеющих общий дизайн, тремя самыми главными элементами дизайна шрифта являются гарнитура, стиль, размер (или по-английски – typeface, style, size соответственно).

Гарнитура – это групповое свойство шрифта, семейство. Типичными гарнитурами являются Helvetica или sans-serif. В одну гарнитуру обычно входят шрифты разной жирности, размера, но одинаковые по внешнему виду и дизайну.

Стиль – это термин, включающий в себя толщину штриха шрифта (или жирность) и наклон шрифта (roman – прямой шрифт, oblique – наклоненный, italic – курсив), курсив отличается от наклоненного шрифта тем, что он специально разработан с наклоном, наклоненный же получается простым наклоном букв вправо. Вот пример:

пример стиля шрифта
На этом рисунке в первой строчке изображены четыре буквы шрифта Times New Roman, первая и третья набраны без наклона, вторая и четвертая курсивом. Сразу бросается в глаза, что буквы набранные курсивом имеют совершенно другую форму, чем набранные прямым шрифтом. Во второй строке изображены те же буквы, но уже из шрифта Tahoma, видно, что наклонное начертание получено простым наклоном букв вправо.

Размер – это величина характеризующая высоту шрифта, как она определяется можно видеть на рисунке:

размер шрифта

Размер измеряется в особых типографских единицах – пунктах, один пункт приблизительно равен 1/72 дюйма (≈0,353мм).

Cемейства шрифтов

Семейство объединяет в себя шрифты с похожими стилями. В GDI определены константы для семйств шрифтов: FF_DECORATIVE, FF_DONTCARE, FF_MODERN, FF_ROMAN, FF_SCRIPT, и FF_SWISS.
FF_DECORATIVE декоративные шрифты, например Old English Old English
FF_DONTCARE используется, когда не важно какой шрифт использовать, берется шрифт по умолчанию.  
FF_MODERN моноширинный шрифт, например Courier New Courier New
FF_ROMAN пропорциональный (буквы разной ширины) шрифт с засечками на концах букв, например, Times New Roman Times New Roman
FF_SCRIPT шрифт, выглядящий как рукописный, например, Kursiv95 Kursiv95
FF_SWISS пропорциональный шрифт без засечек, например, Verdana Verdana

Виды шрифтов

Шрифты бывают векторные (состоящие из отрезков прямых), растровые (состоящие из точек), TrueType, PostScript. Два последних вида являются самыми распространенными форматами и в них символы шрифта хранятся как набор кривых. Кроме того, в каждом шрифте содержится ограниченное число образов букв (так называемых глифов) и поэтому не всегда шрифт можно использовать для вывода национальных символов. Практически в любом шрифте присутствует около сотни стандартных глифов, соответствующих латинскому алфавита, знакам препинания и други служебным символам.

Создание шрифтов

При создании шрифта программист специальным образом указывает свои пожелания по виду, размеру, стилю шрифта, а GDI строит соответствующий логический шрифт и предоставляет программисту его описатель. Существует несколько функций для создания шрифта: ChooseFont, CreateFont, CreateFontIndirect. Первая показывает стандартный диалог выбора шрифта, вторая получает 14 параметров и на их основе создает шрифт, третья аналогично второй, только параметры получает из записи типа TLogFont. Именно третью функцию мы и будем рассматривать подробно.
{ Logical Font }
LF_FACESIZE = 32;
function CreateFontIndirect(const p1: TLogFont): HFONT;
TLogFontA = packed record
    lfHeight: Longint;
    lfWidth: Longint;
    lfEscapement: Longint;
    lfOrientation: Longint;
    lfWeight: Longint;
    lfItalic: Byte;
    lfUnderline: Byte;
    lfStrikeOut: Byte;
    lfCharSet: Byte;
    lfOutPrecision: Byte;
    lfClipPrecision: Byte;
    lfQuality: Byte;
    lfPitchAndFamily: Byte;
    lfFaceName: array[0..LF_FACESIZE - 1] of AnsiChar;
end;

lfHeight
Высота шрифта, всегда задается в точках, для преобразования пунктов в точки используйте формулу
MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
Если задать параметр равным нулю, то используется значение по умолчанию, если меньше нуля, то абсолютное значение определяет высоту «чистого» символа (без выступающих элементов букв, например, Й, Ё, Î, Ã). Все станет понятнее после разбора примера ниже по тексту;
lfWidth
ширина символов в логических единицах, если равна нулю, то выбор шрифта происходит только с учетом высоты;
lfEscapement
угол наклона всей строки в десятых долях градуса против часовой стрелки (за более подробной информацией обращайтесь в справку по win32api);
lfOrientation
угол наклона отдельных символов в десятых долях градусов против часовой стрелки;
lfWeight
жирность шрифта, задается числами от 0 до 1000. 0 – используется значение по умолчанию. 400 – нормальная жирность шрифта, 700 – полужирный шрифт. Для большинства шрифтов есть только три значения жирности.
lfItalic
1, если шрифт должен быть наклоненнным (курсивом), 0 – в противном случае.
lfUnderline
1, если шрифт должен быть подчеркнутым;
lfStrikeOut
1, если шрифт должен быть перечеркнутым;
lfCharSet
определяет набор символов шрифта (сharacter set), для русского языка используйте константу RUSSIAN_CHARSET, для кодировки по умолчанию используйте DEFAULT_CHARSET. Будьте уверены указывая тип набора символов для шрифта, что этот набор ы шрифте присутствует.
lfOutPrecision
точность вывода символов шрифта. Вы можете использовать константу OUT_DEFAULT_PRECIS для значения по умолчанию или OUT_TT_ONLY_PRECIS для шрифтов TrueType.
lfClipPrecision
определяет как будут себя вести символы, если они попадают за область отсечения, рекомендуется ставить CLIP_DEFAULT_PRECIS;
lfQuality
качество вывода шрифта. Константа DEFAULT_QUALITY определяет обычное качество, PROOF_QUALITY – повышенное качество, ANTIALIASED_QUALITY – сглаженный шрифт (если это допускается устройством вывода и шрифтом).
lfPitchAndFamily
в двух младших разрядах указывается тип шрифта (DEFAULT_PITCH, FIXED_PITCH, VARIABLE_PITCH), в четырех старших семейство (FF_DECORATIVE, FF_DONTCARE, FF_MODERN, FF_ROMAN, FF_SCRIPT, FF_SWISS). Вы можете объединять побитовым OR любую константу из первой группы с любой из второй. Это поле используется, если не задано точное название шрифта.
lfFaceName
точное имя шрифта, длина этой строки не более 32 символов, если здесь пустая строка, то GDI берет первый попавшийся шрифт, удовлетворяющий предыдущим пунктам

Вот пример использования этой функции. (скачать можно отсюда)
var 
   dc : HDC;
   fnt, oldFnt : HFONT;
   lf : TLogFont;

...

dc := GetDC(wnd);//получаем контекст окна
lf.lfHeight := -30;{высоту задаем в пикселах, поэтому и отрицательное число,
    если бы мы хотели задать размер в пунктах, то написали бы:
lf.lfHeight := MulDiv(PointSize, GetDeviceCaps(dc, LOGPIXELSY), 72);}
lf.lfWidth := 0;        //игнорируем ширину
lf.lfEscapement := -90;	//угол наклона строки - 9 градусов от оси X вниз
lf.lfOrientation := 0;
lf.lfWeight := 400;     //нормальная жирность
lf.lfItalic := 0;
lf.lfUnderline := 0;
lf.lfStrikeOut := 0;
lf.lfCharSet := RUSSIAN_CHARSET;
lf.lfOutPrecision := 0;
lf.lfClipPrecision := 0;
lf.lfQuality := ANTIALIASED_QUALITY;
lf.lfFaceName := 'Arial';//имя используемого шрифта

fnt := CreateFontIndirect(lf);  //создаем шрифт

oldFnt := SelectObject(dc, fnt);//выбираем шрифт в контекст

SetBkMode(dc, TRANSPARENT);		//делаем фон под буквами прозрачным

TextOut(dc, 100, 100, 'Sample Пример', 13);//печатаем строку

SelectObject(dc, oldFnt);		//возыращаем в контекст старый шрифт
ReleaseDC(wnd, dc);			//освобождаем контекст
Вот как это выглядит:

пример использования CreateFont

Режим отображения

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

function SetMapMode(DC: HDC; fnMapMode: Integer): Integer;

fnMapMode может принимать следующие значения:

MM_TEXT
режим по умолчанию, логическая единица равна одному пикселю устройства, ось X направлена слева направо, ось Y сверху вниз;
MM_LOENGLISH
логическая единица равна 0,01 дюйма (≈0,254мм), ось X слева направо, ось Y снизу вверх;
MM_LOMETRIC
логическая единица равна 0,1мм, ось X слева направо, ось Y снизу вверх;
MM_HIENGLISH
логическая единица равна 0,001 дюйма (≈0,0254мм), ось X слева направо, ось Y снизу вверх;
MM_HIMETRIC
логическая единица равна 0,01мм, ось X слева направо, ось Y снизу вверх;
MM_ANISOTROPIC
логическая единица равна произвольному значению по оси X и Y; направления и масштаб осей выбираются при помощи функций SetWindowExtEx и SetViewportExtEx;
MM_ISOTROPIC
логическая единица равна произвольному значению и одинакова для обеих осей; направления и масштаб осей выбираются при помощи функций SetWindowExtEx и SetViewportExtEx;
MM_TWIPS
логическая единица равна 1/12 пункта принтера (1/1440 дюйма ≈0,01764 мм), ось X слева направо, ось Y снизу вверх.

Получить текущее значение режима можно при помощи функции GetMapMode:

function GetMapMode(DC: HDC): Integer;

Режимом по умолчанию является режим MM_TEXT, когда логические единицы совпадают с физическими. Например, если dc определяет контекст экрана монитора, тогда вызов функции:
TextOut(dc, 10, 20, 'Sample', 6);

приведёт к появлению текста Sample на расстоянии 10 пикселов вправо и 20 пикселов вниз от верхнего левого угла рабочей области. По умолчанию начало координат всегда расположено в верхнем левом углу рабочей области.

Область вывода

Режим отображения определяет, как GDI преобразует логические координаты (параметры функций рисования, например) в физические координаты устройства. Далее используется терминология Windows: логические координаты – «window», координаты устройства – «viewport». Я буду далее в тексте переводить их так же, как и в русском переводе книги Ч. Петзольда, «окно» и «область вывода». Слово «окно» в таких кавычках далее будет обозначать именно логические координаты.

Режим отображения определяет, как GDI преобразует логические координаты (параметры функций рисования, например) в физические координаты устройства. Для преобразования координат устройства в логические координаты существует функция DPtoLP:

function LPtoDP(DC: HDC; var Points; Count: Integer): BOOL;

здесь Points – это массив записей типа TPoint, Count – количество точек в массиве. Для обратного преобразования (логические точки в физические) существует функция LPtoDP:

function LPtoDP(DC: HDC; var Points; Count: Integer): BOOL;

параметры Points и Count имеют такой же смысл, что и в предыдущем случае.

По умолчанию начало координат находится в верхнем левом углу области. Для сдвига начала координат существуют две функции SetViewportOrgEx и SetWindowOrgEx. Вы должны использовать только одну из этих функций, чтобы не запутаться. Вот как нужно работать с первой из них. Пусть вам нужно перенести начало отсчета с верхнего левого угла в центр области, ширина области dx, высота области dy.

перенос начала отсчета

На рисунке область отмечена пунктиром. Перенос делается так:
SetViewportOrgEx (dc, dx div 2, dy div 2, nil);

Теперь логическая точка (0,0) будет отображаться в физическую точку с координатами (dx div 2, dy div 2) и можно рабочую область использовать так, как будто она имеет систему координат с началом в точке (dx div 2, dy div 2). Предполагается, что текущий режим отображения MM_TEXT, то есть логическая единица совпадает с физической. Аргументы функции SetViewportOrgEx всегда задаются в физических координатах (координатах устройства), аргументы функции SetWindowOrgEx всегда задаются в логических координатах (координатах окна). После вызова SetWindowOrgEx:
														  
SetViewportOrgEx (dc, newX, newY, nil);

логическая точка (–newX, –newY) cоответствует физической точке (0,0), т.е. левому верхнему углу рабочей области. Не следует использовать одновременно функции SetWindowOrgEx иSetViewportOrgEx до тех пор, пока вы полностью не разберетесь что делаете.

Как можно заметить в случае режима MM_TEXT эти две функци в некотором роде обратимы, однако в других режимах это не верно! Вызов SetViewportOrgEx выглядит аналогично (т.к. использует физические координаты), а вызов SetWindowOrgEx гораздо сложнее.

О других режимах отображения вы можете прочитать в справке по win32api или в книге Ч. Петзольда «Programming Windows».

Процесс рисования

сообщение WM_PAINT

Существует два способа рисования. Во-первых, описатель контекста устройства можно получить в любой точке программы, воспользовавшись функциями GetDC или GetWindowDC. Во-вторых, описатель можно получать в сообщении WM_PAINT (если вы рисуете, например, на принтере, то никакого сообщения WM_PAINT, понятно, нет). Рисунок получаемый при помощи первого способа временный, то есть он будет уничтожен при первой же перерисовке окна, используя второй способ, вы как раз и занимаетесь перерисовкой. Что же такое перерисовка? На рабочем столе Windows одновременно находятся несколько окон, которые еще и перекрывают друг друга. Вот, например, на рисунке одно окно частично перекрывет другое.

одно окно перекрывает другое

Что произойдет, когда окно с заголовком window 2 оттащить в сторону? В этом случае та часть окна, которая ранее была невидима, получит статус поврежденной или по-английски invalidate region. На рисунке она обведена синей рамкой. После этого GDI формирует и посылает два сообщения, информирующие окно, что поврежденную область надо перерисовать, вот эти сообщения WM_PAINT и WM_NCPAINT. Первое предназначается для перерисовки клиентской части окна, второе неклиентской (например, заголовка или полос прокрутки). Если ваша программа не перехватывает сообщение WM_PAINT, то окно закрашивается той кистью, которая была определена при создании класса окна, например:

wc.hbrBackground:=COLOR_BTNFACE+1;

В данном случае окно будет закрашено тем же цветом, что и фон кнопки. Сообщение WM_PAINT не может быть послано вручную, оно всегда генерируется системой. В обработчике этого сообщения всегда должны присутствовать функции BeginPaint и EndPaint, причем именно в таком порядке. После обработки этого сообщения оконная процедура должна возвратить нуль.
var ps : TPaintStruct;
dc : HDC;
...
WM_PAINT: begin
	dc := BeginPaint(wnd, ps);

	EndPaint(wnd, ps);
	result := 0;
end;

Переменная типа TPaintStruct предназначена для получения информации о рисовании. Этот тип является записью, которая определена в файле windows.pas следующим образом:
  TPaintStruct = packed record
    hdc: HDC;
    fErase: BOOL;
    rcPaint: TRect;
    fRestore: BOOL;
    fIncUpdate: BOOL;
    rgbReserved: array[0..31] of Byte;
  end;

Первый параметр hdc содержит описатель графического контекста, причем этот же описатель возвпащается функцией BeginPaint. Второй параметр fErase, определяет будет ли перерисовываться фон окна, если он равен TRUE, то GDI сама перерисует фон, использую текущую кисть класса окна. Третий параметр rcPaint определяет поврежденный прямоугольник, значения заданы в пикселях относительно верхнего левого угла рабочей области. Остальные три параметре не документированы и предназначены для внутреннего использования Windows. Таким образом, вам не нужно каждый раз перерисовывать всю область окна, а лишь ту его часть, что задана в параметре rcPaint. Вот пример использования сообщения WM_PAINT и поля rcPaint (скачать можно отсюда). Обработчик сообщения там очень простой – мы просто рисуем на месте поврежденного прямоугольника прямоугольник при помощи функции Rectangle, рисуем текущим пером (черным) и кистью (сплошная белая).
WM_PAINT: begin
   dc := BeginPaint(wnd, ps);//получаем контекст
       //рисуем прямоугольник
   Rectangle(dc, ps.rcPaint.Left, ps.rcPaint.Top, ps.rcPaint.Right, ps.rcPaint.Bottom);
   EndPaint(wnd, ps);//завершаем рисование
   result := 0;
end;

Как работает эта программа? Если вы посмотрите на исходный текст, то увидите, что кисть для закрашивания фона поставлена цвета фона кнопки, а окно имеет белый фон. Дело все в том, что когда окно создается и появляется на экране, то вся клиентская область нуждается в перерисовке (а, точнее, в первоначальной нарисовке), поэтому вся клментская часть объявляется как поврежденный прямоугольник и соответственно на его месте рисуется наш прямоугольник с черной рамкой и белам фоном. Точно такая же полная перерисовка присходит и в случае, когда окно целиком покрывается другим. Самое же интересное происходит, когда перекрывается только часть окна, специально для демонстрации этого случая в программе создано ещё одно вспомогательное окно. Если вы будете перетаскивать это окно, то на том месте, где оно только что было, увидите прямоугольник c черной рамкой:

пример программы демонстрирующей использование WM_PAINT

Если вы попробуете изменить размер окна, то увидите, что все нарисованное исчезло. Это происходит оттого, что при создании класса окна мы выставили в записи TWndClassEx поле style таким образом:
wc.style:= CS_HREDRAW or CS_VREDRAW;

и тем самым указали системе полностью перерисовывать окно при изменении горизонтального и вертикального размеров окна.

Функции рисования линий

функция SetROP2

Это очень важная функция, она позволяет установить как будет присходить рисование, а точнее, как фон будет влиять на цвет рисуемого объекта. SetRop2 определяет логическре правило по которому будет вычисляться итоговый цвет в зависимости от цвета пера (кисти) и цвета пиксела под рисуемым объектом. Заголовок функции следующий:

function SetROP2(DC: HDC; fnDrawMode: Integer): Integer;

где fnDrawMode одна из следующих констант, определяющая, какой в итоге получится цвет пиксела, исходя из цвета фона и переднего плана.

R2_BLACK
пиксел всегда черный (равен 0);
R2_COPYPEN
пиксел цвета пера (по умолчанию) (P);
R2_MASKNOTPEN
итоговый цвет – комбинация цвета фона и инвертированного цвета пера (~P & D);
R2_MASKPEN
итоговый цвет – комбинация увета фона и пера (P & D);
R2_MASKPENNOT
итоговый цвет – комбинация инвертированного цвета фона и цвета пера (P & ~D);
R2_MERGENOTPEN
итоговый цвет – слияние цвета фона и инвертированного цвета пера (~P | D);
R2_MERGEPEN
итоговый цвет – слияние цвета фона и цвета пера (P | D);
R2_MERGEPENNOT
итоговый цвет – слияние инвертированного цвета фона и цвета пера (P | ~D);
R2_NOP
цвет остается без изменений (D);
R2_NOT
цвет становится равным инвертированному цвету фона (~D);
R2_NOTCOPYPEN
цвет становится равным инвертированному цвету пера (P);
R2_NOTMASKPEN
цвет становится равным инвертированному цвету R2_MASKPEN (~(P & D));
R2_NOTMERGEPEN
цвет становится равным инвертированному цвету R2_MERGEPEN (~(P | D));
R2_NOTXORPEN
цвет становится равным инвертированному цвету R2_XORPEN (~(P ^ D));
R2_WHITE
цвет пиксела всегда белый;
R2_XORPEN
цвет пиксела получается комбинацией цветов фона и пера по правилу «исключающее ИЛИ»(P ^ D).

Режимом по умолчанию является режим R2_COPYPEN, остальный режимы позволяют добиться многих интересных эффектов. Программу, демонстрирующую использование этой функции можно скачать отсюда. Вот скриншот с неё:

пример программы, демонстрирующей использование функции SetRop2

В этой программе через каждые 20 пикселов проводится вертикальная линия одним и тем же пером синего цвета, толщиной 12 пикселов. Горизонтальная линия проведена тем же пером при значении по умолчанию. Цифра соответствует порядковому номеру соответствующей R2_ константе в соответствии с приведённым списком. Вы можете сами сделать выводы о том как именно действует SetRop2.

отдельные пикселы и прямые

Для рисования одного пиксела заданного цвета используется функция SetPixel:

function SetPixel(DC: HDC; X, Y: Integer; crColor: COLORREF): COLORREF;

crColor определяет желаемый цвет, возвращается цвет, которым в действительности был нарисован пиксель. Цвет получившегося пиксела устанавливается максимально приближенным к crColor и выбирается из доступных устройству.

Для рисования прямой линии служит функция LineTo:

function LineTo(DC: HDC; X, Y: Integer): BOOL;
Она проводит прямую из текужего положения пера до точки (X,Y). Для рисования используется текущее перо. После выполнения этой функции перо нходится в точке (X,Y). Для прямого изменения позиции пера используется функция MoveToEx:
function MoveToEx(DC: HDC; X, Y: Integer; lpPoint: PPoint): BOOL;

она перемещает перо в позицию (X,Y), а координаты старого положения пера сохраняет в переменной lpPoint, которая является указателем на переменную типа TPoint, если этот параметр равен nil, то прежняя позиция не возвращается. Именно эта пара функций использовалась в программе демонстрирующей возможности SetRop2, вот фрагмент из неё:
dc := BeginPaint(wnd, ps);//начинаем отрисовку в соощении WM_PAINT
pen := CreatePen(PS_SOLID, 12, RGB(0, 0, 255));//создаем перо толщины 12 синего цвета
oldPen := SelectObject(dc, pen); //выбираем перо в контекст, запоминаем старое перо
MoveToEx(dc, 0, 40, nil); //перемещаем текущую позицию пера
rop := SetRop2(dc, R2_COPYPEN);

LineTo(dc, 400, 40);   //проводим линию

MoveToEx(dc, 10, 40, nil);

for i:=1 to 16 do begin
   SetRop2(dc, rops[i]);//в массиве rops[] хранятся 16 констант R2_XXX
   MoveToEx(dc, 20*i, 10, nil);
   LineTo(dc, 20*i, 80);
   s := inttostr(i);
   if i<10 then TextOut(dc, 20*i-4, 90, @s[1], 1)
   else TextOut(dc, 20*i-8, 90, @s[1], 2)
end;
SetRop2(dc, rop);//восстанавливаем прежнее значение 
SelectObject(dc, oldPen);
EndPaint(wnd, ps);//заканчиваем отрисовку

Для получения текущих координат пера используется функция GetCurrentPositionEx:
var pt : TPoint;
...
GetCurrentPositionEx(dc, @pt);
//теперь в pt хранится положение пера

Для рисования ломаных линий существует функции Polyline, PolylineTo и PolyPolyline:

function Polyline(DC: HDC; var Points; Count: Integer): BOOL;
function PolyLineTo(DC: HDC; const Points; Count: DWORD): BOOL;
function PolyPolyline(DC: HDC; const PointStructs; const Points; p4: DWORD): BOOL;

здесь Points – массив из Count точек (переменных типа TPoint). Функция Polyline просто последовательно соединяет точки, перечисленные в массиве прямыми, не меняя при этом положение пера. Функция PolylineTo использует в качестве стартовой точки не первую точку массива, а текущее положение пера, после её выполнения текущее положение указателя перемещается в последнюю из точек массива. Функция PolyPolyline предназаначена для рисования нескольких не связанных между собой ломаных. Чтобы нарисовать замкнутый многоугольник, используйте Polyline, при этом в массиве точек первая должна совпадать с последней.

дуги

Для рисования дуги эллипса существуют функция Arc:

function Arc(hDC: HDC; xLeft, yTop, xRight, yBottom, startX, startY, endX, endY: Integer): BOOL;

Дуга рисуется так, как показано на рисунке:

как строится дуга

Координаты xLeft, yTop, xRight, yBottom определяют прямоугольник, ограничивающий эддипс, дуга которого строится. startX, startY – это координаты точки, определяющей начальную точку дуги, GDI начинает дугу с точки пересечения эллипса (вписанного в данный прямоугольник) и прямой, проходящей через центр эллипса и точку (startX, startY). Соответственно endX, endY определяют конечную точку дуги аналогичным образом. Дуга всегда рисуется в направлении против часовой стрелки. Для рисования используется текущее перо. Вот пример того, как рисуется дуга (скачать здесь).
dc := BeginPaint(wnd, ps);

//создаем перо коричневого цвета, толщиной 2
pen := CreatePen(PS_SOLID, 2, RGB($a1, $70, $1b));
oldPen := SelectObject(dc, pen);

//(105, 55) - центр эллипса
//(150,120) - начальная точка
//(125,10)  - конечная точка

Arc(dc, 10, 10, 200,100, 150, 120, 250, 10); //дуга
SelectObject(dc, oldPen);

//строим линии к точкам, чтобы показать как строится дуга
MoveToEx(dc, 105, 55, nil);
LineTo(dc, 150, 120);

MoveToEx(dc, 105, 55, nil);
LineTo(dc, 250, 10);

EndPaint(wnd, ps);

пример программы строящей дуги

Для рисования окружности тоже используется функция Arc, нужно только указать одинаковую начальную и стартовую точки.

сплайны Безье

Для построения более сложных линий, чем дуги и прямые используются так называемые сплайны Безье. Они предназаначены для аппроксимации самых разных кривых с достаточной степенью точности. Формулы, по которым строятся сплайны, вы можете найти в любом учебнике по компьютерной графике. Плоский сплайн Безбе определяется четырьмя точками (две – начальная и конечная, и две контрольные), неформально две контрольные точки можно представить магнитами, которые оттягивают на себя прямую, соединяющую начальную и конечную точки.

сплайн Безье

Для построения сплайнов в Windows есть функция PolyBezier:

function PolyBezier(DC: HDC; const Points; Count: DWORD): BOOL;

Здесь параметр Points – это массив переменных типа TPoint, Count количество кривых, которые нужно построить. Первая точка в массиве Points определяет точку, из которой начнется построение первой кривой, следующие две точки – контрольные, четвёртая – конечная точка первой кривой. Далее для каждой последующей кривой нужно только три точки, первой точкой следующей кривой является последняя точка предыдущей. Линия строится текущим пером. Вот пример (скачать здесь)
//(10,10)   - начальная точка
//(250,250) - конечная точка
//(170,35) и (5,250) - контрольные точки

var
    apt : array[0..3] of TPoint = ((x:10;y:10), (x:170;y:35),
                                   (x:5;y:250), (x:250;y:250));
...
WM_PAINT: begin
    dc := BeginPaint(wnd, ps); 
    PolyBezier(dc, apt, 4); //рисуем один сплайн

    //рисуем узловые (контрольные) точки красным цветом
    SetPixel(dc, apt[1].x, apt[1].y, RGB(255, 0, 0));
    SetPixel(dc, apt[2].x, apt[2].y, RGB(255, 0, 0));
    EndPaint(wnd, ps);
    result := 0;
end;

пример программы, использующей функцию PolyBezier

Функции рисования закрашенных фигур

эллипс

Я не буду вдаваться здесь в подробности из аналитической геометрии, скажу только, что эллипс, это геометрическое место точек, сумма расстояний от которых до двух фиксированных точек остается постоянной. В Windows для построения закрашенного эллипса используется функция Ellipse:

function Ellipse(DC: HDC; X1, Y1, X2, Y2: Integer): BOOL;

Эллипс

Эллипс строится по ограничивающему его прямоугольнику, то есть вы можете построить только эллипс, с осями, параллельными осям координат. Ограничивающий прямоугольник – это минимальный прямоугольник полностью окружающий в данном случае эллипс. Граница эллипса рисуется текущим пером, внутренность заполняется текущей кистью.

сегмент (chord)

Сегмент – это часть эллипса, отсеченная прямой, называемой секущей. Для рисования сегмента существует функция Chord:

function Chord(DC: HDC; xLeft, yTop, xRight, yBottom, startX, startY, endX, endY: Integer): BOOL;

Сегмент

Здесь xLeft, yTop, xRight, yBottom – координаты ограничивающего прямоугольника, котрый определяет эллипс, из которого, в свою очередь, вырезается сегмент. startX, startY, endX, endY определяют дугу, которая ограничивает сегмент. Делается это точно так же, как и в функции Arc. Граница сегмента рисуется текущим пером, внутренность заполняется текущей кистью.

сектор (pie)

Сектор – это часть эллипса, отсеченная двумя его радиусами. Для рисования сектора существует функция Pie:

function Pie(DC: HDC; xLeft, yTop, xRight, yBottom, startX, startY, endX, endY: Integer): BOOL;

Сектор

Здесь xLeft, yTop, xRight, yBottom – координаты ограничивающего прямоугольника, котрый определяет эллипс, из которого, в свою очередь, вырезается сектор. startX, startY, endX, endY определяют дугу, которая ограничивает сектор. Делается это точно так же, как и в функции Arc. Граница сектора рисуется текущим пером, внутренность заполняется текущей кистью.

многоугольник

Многоугольник – это замкнутая фигура, граница которой состоит из прямых. Для рисования многоугольников существует функция Polygon:

function Polygon(DC: HDC; var Points; Count: Integer): BOOL;

Здесь Points массив состоящий из переменных типа TPoint, и определяющий вершины многоугольника; Count – число вершин многоугольника, должно быть больше либо равно двум. Последняя построенная вершина автоматически соединяется с первой. Граница многоугольника рисуется текущим пером, внутренность заполняется текущей кистью. Если в многоугольнике нет самопересечений, то он просто заполняется текущей кистью; если же самопересечения есть, то режим закрашивания определяется при помощи функции SetPolyFillMode:

function SetPolyFillMode(DC: HDC; PolyFillMode: Integer): Integer;

Здесь PolyFillMode – режим закрашивания многоугольников. Может принимать два значения:

ALTERNATE
будут закрашены только те области, которые лежат между четными и нечетными сторонами многоугольника по каждой линии развертки. Это режим по умолчанию;
WINDING
В этом режиме для того чтобы определить, надо ли закрашивать область многоугольника, учитывается направление, в котором был нарисован этот многоугольник. Каждая сторона многоугольника может быть нарисована в направлении либо по часовой стрелке, либо против часовой стрелки. Если воображаемая линия, нарисованная в направлении из внутренней области многоугольника в наружную, пересекает сегмент, нарисованный в направлении по часовой стрелке, содержимое некоторого внутреннего счетчика увеличивается на единицу. Если же эта линия пересекает сегмент, нарисованный против часовой стрелки, содержимое счетчика уменьшается на единицу. Область закрашивается только в том случае, если содержимое счетчика не равно нулю

Вот пример использования этих функций (скачать можно здесь) и скриншот программы:
WM_PAINT: begin
    dc := BeginPaint(wnd, ps);
    Polygon(dc, apt, 6);//рисуем многоугольник
    SetPolyFillMode(dc, WINDING);//меняем режим закраски
    for i:=0 to 5 do inc(apt[i].x, 300);//сдвигаем все точки на 300 пикселов
    Polygon(dc, apt, 6);//еще раз рисуем
    for i:=0 to 5 do dec(apt[i].x, 300); //сдвигаем назад
    EndPaint(wnd, ps);
end;

пример использования функций Polygon и SetPolyFillMode

прямоугольник

Для рисования прямоугольника существует функция Rectangle:

function Rectangle(DC: HDC; X1, Y1, X2, Y2: Integer): BOOL;

Здесь X1,Y1 – верхний левый угол прямоугольника; X2,Y2 – нижний правый. Граница рисуется текущим пером, внутренность заполняется текущей кистью.

Для рисования прямоугольника со скругленными углами используется функция RoundRect:

function RoundRect(DC: HDC; X1, Y1, X2, Y2, Width, Height: Integer): BOOL;

Здесь X1,Y1 – верхний левый угол прямоугольника; X2,Y2 – нижний правый, размер скругления определяется параметрами Width и Height:

прямоугольник со скругленными углами

Назад|Содержание|Вперед

Хостинг от uCoz