2. Работа с контекстом отображения

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

Как правило, приложения выполняют всю работу по рисованию во время обработки сообщения WM_PAINT, хотя часто требуется рисовать и во время обработки других сообщений. В любом случае приложение должно придерживаться следующей последовательности действий:

Последнее действие (освобождение или удаление контекста отображения) должно быть обязательно выполнено. Самый простой способ полностью нарушить работоспособность Windows - забыть освободить полученный контекст отображения или удалить созданный контекст отображения или устройства.

Так как контекст отображения - критический ресурс, его необходимо освобождать сразу, как только в нем отпадет необходимость. Операционная система Windows выполняет кеширование обычного контекста отображения (есть и необычные контексты отображения, но об этом позже), причем кешируются только пять контекстов. Если Windows не может удовлетворить запрос какого-либо приложения на выделение контекста отображения, вся операционная система окажется в критическом состоянии, единственным выходом из которого будет полный перезапуск Windows.

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

Однако прежде всего необходимо научиться получать и освобождать контекст отображения.

2.1. Получение и освобождение контекста отображения

Способы получения (и, соответственно, освобождения) контекста отображения разные для контекстов разного типа. Можно выделить следующие типы контекста отображения:

Каждый из перечисленных выше контекстов имеет свои особенности и свое назначение.

Общий контекст отображения

Этот контекст используется чаще всего и поэтому для ускорения доступа к нему Windows использует кеширование (как мы уже говорили, размер кеша достаточен для хранения только пяти контекстов отображения).

Для получения общего контекста отображения приложение должно вызвать функцию BeginPaint (при обработке сообщения WM_PAINT ) или GetDC (при обработке других сообщений). При этом перед регистрацией класса окна в поле стиля класса окна в структуре WNDCLASS не должны использоваться значения CS_OWNDC, CS_PARENTDC или CS_CLASSDC :

wc.style = 0;

Функция BeginPaint возвращает контекст отображения для окна hwnd:

HDC WINAPI BeginPaint(HWND hwnd, PAINTSTRUCT FAR* lpps);

Перед этим она подготавливает указанное окно для рисования, заполняя структуру типа PAINTSTRUCT (адрес которой передается через параметр lpps) информацией, которую можно использовать в процессе рисования.

Структура PAINTSTRUCT и указатели на нее (различных типов) описаны в файле windows.h:

typedef struct tagPAINTSTRUCT
{
  HDC  hdc;
  BOOL fErase;
  RECT rcPaint;
  BOOL fRestore;
  BOOL fIncUpdate;
  BYTE rgbReserved[16];
} PAINTSTRUCT;
typedef PAINTSTRUCT* PPAINTSTRUCT;
typedef PAINTSTRUCT NEAR* NPPAINTSTRUCT;
typedef PAINTSTRUCT FAR* LPPAINTSTRUCT;

Рассмотрим назначение отдельных полей структуры PAINTSTRUCT.

Поле hdc после возврата из функции будет содержать идентификатор полученного контекста отображения, который должен передаваться в качестве параметра функциям интерфейса GDI, выполняющим рисование. Можно использовать также значение идентификатора контекста, возвращенное функцией BeginPaint, так как эти значения одинаковые.

Анализируя содержимое поля fErase, приложение может определить, нужно ли перерисовывать фон окна. Если в этом поле находится значение TRUE, фон окна должен быть перерисован. Такая необходимость может возникнуть в том случае, если в классе, на базе которого создано окно, при регистрации не была выбрана кисть для закрашивания фона (поле hbrBackground структуры WNDCLASS).

Поле rcPaint, которое представляет собой структуру типа RECT, содержит координаты верхнего левого и правого нижнего угла прямоугольника, внутри которого нужно рисовать. Напомним вам формат структуры RECT, описанной в файле windows.h:

typedef struct tagRECT
{
  int left;
  int top;
  int right;
  int bottom;
} RECT;

Мы уже говорили вам в 11 томе "Библиотеки системного программиста", что при обработке сообщения WM_PAINT приложение должно суметь перерисовать все окно или любую его часть. Сообщение WM_PAINT может попасть в функцию окна в том случае, если все окно или его часть требуют перерисовки. Поле rcPaint в структуре PAINTSTRUCT содержит координаты прямоугольной области, расположенной в окне и требующей перерисовки.

Остальные поля зарезервированы для Windows и не используются приложениями.

Контекст отображения, полученный при помощи функции BeginPaint, необходимо освободить перед завершением обработки сообщения WM_PAINT, вызвав функцию EndPaint :

void WINAPI EndPaint(HWND hwnd, const PAINTSTRUCT FAR* lpps);

Функции EndPaint передаются те же параметры, что и функции BeginPaint.

Обычно обработчик сообщения WM_PAINT выглядит следующим образом:

PAINTSTRUCT ps;
HDC hdc;
........
case WM_PAINT:
{
  // Получаем контекст отображения
  hdc = BeginPaint(hwnd, &ps);

// После получения контекста отображения
// можно вызывать функции GDI 
  TextOut(hdc, 0, 0, (LPSTR)"String", 6);
       .
       .
// Освобождаем контекст отображения
  EndPaint(hwnd, &ps);
  break;
}

Подобный фрагмент кода вы можете найти в приложениях, описанных в одном из предыдущих томов "Библиотеки системного программиста".

Функции BeginPaint и EndPaint можно использовать только внутри обработчика сообщения WM_PAINT. Если же приложению требуется рисовать во время обработки других сообщений, оно должно получить контекст отображения с помощью функции GetDC. После завершения процедуры рисования перед выходом из обработчика сообщения следует освободить полученный контекст отображения, вызвав функцию ReleaseDC.

Функция GetDC возвращает контекст отображения для окна с идентификатором hwnd:

HDC WINAPI GetDC(HWND hwnd);

Полученный таким образом контекст отображения можно использовать для рисования во внутренней области окна (window client region).

Функция ReleaseDC освобождает контекст отображения hdc, полученный для окна hwnd:

int WINAPI ReleaseDC(HWND hwnd, HDC hdc);

Мы еще раз обращаем ваше внимание на необходимость своевременного освобождения общего контекста отображения.

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

Контекст отображения для класса окна

Общий контекст отображения, описанный нами в предыдущем разделе, кешируется операционной системой Windows для ускорения доступа к нему. Однако вы можете создать такой контекст отображения, который хранится отдельно в единственном экземпляре и используется всеми окнами, созданными на базе класса окна. При регистрации такого класса окна вы должны указать стиль CS_CLASSDC :

wc.style = CS_CLASSDC;

Все окна, созданные на базе класса, имеющего стиль CS_CLASSDC, будут использовать один общий контекст отображения.

В отличие от общего контекста отображения, приложения, однажды получив контекст отображения для класса окна, могут не освобождать его. То есть для контекста этого типа после функций BeginPaint и GetDC можно не вызывать функции EndPaint и ReleaseDC. Если же приложение вызовет функцию EndPaint или ReleaseDC, они не будут ничего делать и сразу вернут управление. Для уменьшения вероятности ошибки мы рекомендуем вам всегда освобождать контекст отображения, даже если это и не требуется для данного типа контекста.

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

Дело в том, что при получении контекста отображения для окна в нем устанавливаются атрибуты области ограничения и начало системы физических координат устройства отображения. Если на базе нашего класса создано несколько окон, при получении ими контекста отображения эти атрибуты "настраиваются" на окно, получившее контекст отображения. Но если на базе класса стиля CS_CLASSDC приложение создало только одно окно, оно может получить контекст отображения для класса окна один раз и не освобождать его.

Зачем используется контекст отображения класса окна?

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

Личный контекст отображения

Указав в стиле класса окна значение CS_OWNDC, можно добиться того, что для каждого окна, созданного на базе такого класса, Windows создаст отдельную структуру контекста отображения:

wc.style = CS_OWNDC;

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

Однако не будет ошибкой, если приложение будет использовать личный контекст отображения как общий, то есть каждый раз при обработке сообщения WM_PAINT (или другого сообщения, обработчик которого занимается рисованием) оно будет получать контекст и затем отдавать его. Соответствующие функции (BeginPaint, EndPaint, GetDC, ReleaseDC) будут возвращать управление, не выполняя никаких действий.

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

Родительский контекст отображения

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

Для использования родительского контекста отображения в классе, на базе которого создается дочернее окно, перед регистрацией необходимо указать стиль CS_PARENTDC :

wc.style = CS_PARENTDC;

Контекст отображения для окна

Все описанные выше контексты отображения позволяют рисовать только во внутренней области окна (client region). С помощью функции GetWindowDC приложение может получить контекст отображения для окна, позволяющий рисовать в любом месте окна (в области заголовка окна, в области системного меню, рамки окна, кнопок изменения размера и т. п.).

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

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

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

Контекст физического устройства

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

В отличие от контекста отображения, контекст физического устройства не получается, а создается, для чего используется функция CreateDC :

HDC WINAPI CreateDC(
  LPCSTR lpszDriver,            // имя драйвера
  LPCSTR lpszDevice,            // имя устройства
  LPCSTR lpszOutput,            // имя файла или порта вывода
  const void FAR* lpvInitData); // данные для инициализации

Параметр lpszDriver является указателем на строку символов, содержащую имя драйвера, обслуживающего физическое устройство. Имя драйвера совпадает с именем файла *.drv, содержащего сам драйвер и расположенного в системном каталоге Windows.

Имя устройства lpszDevice - это название самого устройства, описанное, например, в файле win.ini в разделе [devices].

Параметр lpszOutput указывает на структуру данных типа DEVMODE, используемую при инициализации устройства вывода. Если при работе с устройством нужно использовать параметры, установленные при помощи приложения Control Panel, параметр lpszOutput следует указать как NULL.

Более подробно вопросы работы с принтером будут рассмотрены в отдельной главе этого тома.

В приведенном ниже примере создается контекст устройства для лазерного принтера HP Laserjet III, подключенного к порту LPT1:, причем в системном каталоге Windows для этого принтера установлен драйвер hppcl5a.drv:

hdc = CreateDC("hppcl5a", "HP LaserJet III", "LPT1:", NULL);

Аналогично, для принтера Epson FX-850, подключенного к порту LPT2:, и работающему через драйвер epson9.drv:

hdc = CreateDC("epson9", "Epson FX-850", "LPT2:", NULL);

Созданный при помощи функции CreateDC контекст устройства следует удалить (но не освободить), вызвав функцию DeleteDC:

BOOL WINAPI DeleteDC(HDC hdc);

Эта функция возвращает TRUE при нормальном завершении и FALSE при возникновении ошибки.

Контекст для устройства DISPLAY

В некоторых случаях требуется получить контекст отображения, позволяющий приложению рисовать в любом месте экрана дисплея. Такой контекст можно создать при помощи функции CreateDC, указав в качестве имени драйвера строку "DISPLAY ", а в качестве остальных параметров - значение NULL:

hdc = CreateDC("DISPLAY", NULL, NULL, NULL);

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

После использования созданный таким образом контекст отображения следует удалить, вызвав функцию DeleteDC.

Есть еще два способа получения контекста для экрана.

Приложение может получить контекст отображения для всего экрана при помощи функции GetDC, указав в качестве параметра значение NULL:

hdc = GetDC(NULL);

Полученный таким образом контекст следует освободить после использования при помощи функции ReleaseDC, передав ей вместо идентификатора окна значение NULL:

ReleaseDC(NULL, hdc);

Еще один способ связан с использованием функции GetDCEx, описание которой будет приведено ниже.

Информационный контекст

Если приложению необходимо получить информацию об устройстве вывода (например, с помощью функции GetDeviceCaps, рассмотренной нами в 11 томе "Библиотеки системного программиста"), оно может создать вместо обычного информационный контекст. Информационный контекст нельзя использовать для рисования, однако он занимает меньше места в памяти.

Информационный контекст создается при помощи функции CreateIC, аналогичной по своим параметрам функции CreateDC:

HDC WINAPI CreateIC(
  LPCSTR lpszDriver,            // имя драйвера
  LPCSTR lpszDevice,            // имя устройства
  LPCSTR lpszOutput,            // имя файла или порта вывода
  const void FAR* lpvInitData); // данные для инициализации

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

Контекст для памяти

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

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

HDC WINAPI CreateCompatibleDC(HDC hdc);

Созданный таким образом контекст памяти удаляется при помощи функции DeleteDC.

Использование контекста памяти будет подробно описано в главе, посвященной битовым изображениям bitmap.

Контекст для метафайла

Контекст для метафайла позволяет записывать команды GDI в файл и затем проигрывать такой файл на физическом устройстве вывода. Файл может находиться в памяти или на диске, в последнем случае его можно использовать для переноса графического изображения в другое приложение.

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

HDC WINAPI CreateMetaFile(LPCSTR lpszFileName);

Параметр lpszFileName должен указывать на строку, содержащую путь к имени файла, в который будут записаны команды GDI, или NULL. В последнем случае создается метафайл в оперативной памяти.

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

HMETAFILE WINAPI CloseMetaFile(HDC hdc);

Эта функция закрывает метафайл для контекста hdc и возвращает идентификатор метафайла. Идентификатор закрытого метафайла использовать нельзя, так как он не содержит никакой полезной информации.

Что можно сделать с полученным идентификатором метафайла?

Можно скопировать метафайл в обычный дисковый файл, вызвав функцию CopyMetaFile :

HMETAFILE WINAPI CopyMetaFile(HMETAFILE hmf, 
  LPCSTR lpszFileName);

Параметр hmf определяет метафайл, параметр lpszFileName содержит путь к имени файла, в который будет записан метафайл.

Можно проиграть метафайл в контексте отображения или контексте устройства, вызвав функцию PlayMetaFile :

BOOL WINAPI PlayMetaFile(HDC hdc, HMETAFILE hmf);

Наконец, при помощи функции DeleteMetaFile можно удалить метафайл:

BOOL WINAPI DeleteMetaFile(HMETAFILE hmf);

Удаление метафайла с помощью функции DeleteMetaFile делает недействительным идентификатор метафайла hmf и освобождает оперативную память, занятую метафайлом. Если метафайл был создан как обычный дисковый файл, функция DeleteMetaFile не удаляет его с диска.

Для того чтобы воспользоваться метафайлом, хранящимся в виде дискового файла, его следует загрузить при помощи функции GetMetaFile, указав ей в качестве единственного параметра путь к соответствующему файлу:

HMETAFILE WINAPI GetMetaFile(LPCSTR lpszFileName);

Функция GetDCEx

В программном интерфейсе операционной системы Windows версии 3.1 появилась функция GetDCEx, предоставляющая расширенные возможности для получения контекста отображения:

HDC WINAPI GetDCEx(register HWND hwnd,
  HRGN hrgnClip, DWORD flags);

Функция возвращает идентификатор полученного контекста отображения или NULL при ошибке.

Параметр hwnd задает идентификатор окна, для которого необходимо получить контекст отображения.

С помощью параметра hrgnClip можно определить область ограничения вывода. Эта область может иметь произвольную форму и являться комбинацией нескольких областей ограничения. Использование областей ограничения будет подробно описано дальше в этой главе.

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

Константа Описание
DCX_WINDOW Функция возвращает контекст отображения, позволяющий рисовать во всем окне, а не только в его внутренней области
DCX_CACHE Функция получает общий контекст отображения из кеша Windows, даже если окно создано на базе класса стиля CS_OWNDC или CS_CLASSDC
DCX_CLIPCHILDREN Видимые области всех дочерних окон, расположенных ниже окна hwnd, исключаются из области отображения
DCX_CLIPSIBLINGS Видимые области всех окон-братьев (окон, имеющих общих родителей), расположенных выше окна hwnd, исключаются из области отображения
DCX_PARENTCLIP Для отображения используется вся видимая область родительского окна, даже если родительское окно создано с использованием стилей WS_CLIPCHILDREN и WS_PARENTDC. Начало координат устанавливается в левый верхний угол окна hwnd
DCX_EXCLUDERGN Если указан этот флаг, при выводе будет использована область ограничения, заданная параметром hrgnClip
DCX_INTERSECTRGN Используется пересечение области ограничения, заданной параметром hrgnClip, и видимой области полученного контекста отображения
DCX_LOCKWINDOWUPDATE Этот флаг разрешает рисование в окне, заблокированном для рисования функцией LockWindowUpdate . Флаг можно использовать при необходимости рисовать, например, рамку, выделяющую произвольную область экрана

Контекст отображения, полученный функцией GetDCEx, следует освободить после использования при помощи функции ReleaseDC.

2.2. Выбор режима отображения

Напомним, что режим отображения - это атрибут контекста отображения, влияющий на используемую функциями GDI систему координат. Для обеспечения независимости приложений от аппаратного обеспечения приложения Windows работают с логическими координатами, которые отображаются в физические. Приложения Windows (в отличие от программ MS-DOS) могут не знать номер используемого видеорежима и соответствующее ему разрешение по вертикали и горизонтали в пикселах, определяя размеры элементов формируемого изображения в миллиметрах или дюймах. Хотя в качестве единицы измерения можно использовать и пиксел, если выбрать соответствующий режим отображения.

Из уроков геометрии в школе и институте вы знаете, что существуют различные системы координат, каждая из которых удобна в том или ином случае. Мы не будем описывать все возможные системы координат, ограничившись доступными в Windows версии 3.1 (в операционной системе Windows NT набор систем координат немного расширен).

Изучение режимов отображения Windows версии 3.1 мы начнем с определения основных понятий и терминов, имеющих отношение к системам координат и преобразованию логических координат в физические.

Основные определения

Прежде всего необходимо определить понятия "физические координаты " и "логические координаты ".

Физические координаты, как это следует из названия, имеют непосредственное отношение к физическому устройству вывода. В качестве единицы измерения длины в системе физических координат всегда используется пиксел. Если устройством вывода является экран монитора, физические координаты обычно называют экранными координатами.

Логические координаты передаются функциям GDI, выполняющим рисование фигур или вывод текста. Используемые единицы измерения зависят от режима отображения.

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

Физическая система координат

На рис. 2.1 показана физическая система координат для экрана видеомонитора.

Рис. 2.1. Физическая система координат для экрана видеомонитора

Начало этой системы координат располагается в левом верхнем углу экрана. Ось X направлена слева направо, ось Y - сверху вниз. В качестве единицы длины в данной системе координат используется пиксел.

Для того чтобы определить физическое разрешение устройства вывода (например, размер экрана в пикселах по вертикали и горизонтали), следует использовать функцию GetDeviceCaps, которую мы рассмотрели в 11 томе "Библиотеки системного программиста". Если передать в качестве второго параметра этой функции значения VERTRES и HORZRES, она в любом режиме отображения вернет, соответственно, размер экрана в пикселах по вертикали и по горизонтали. Параметр hdc должен указывать информационный контекст или контекст отображения, связанный с экраном монитора:

HDC  hdc;
int iVertRes, iHorzRes;
hdc = CreateDC("DISPLAY", NULL, NULL, NULL);
iVertRes = GetDeviceCaps(hdc, VERTRES);
iHorzRes = GetDeviceCaps(hdc, HORZRES);
DeleteDC(hdc);

Физическая система координат "привязана" к физическому устройству вывода, поэтому при ее использовании для вывода изображения следует учитывать особенности видеоконтроллера. В 11 томе "Библиотеки системного программиста" в разделе, посвященном метрикам операционной системы Windows, мы подробно рассказали об использовании функции GetDeviceCaps для исследования пространственных характеристик монитора. Для удобства мы воспроизведем приведенную в этом томе таблицу некоторых метрик для видеомониторов различных типов.

Параметр функции GetDeviceCaps CGA EGA VGA SVGA 800 x 600 8514/A SVGA 1024 x 768
HORZRES 640 640 640 800 1024 1024
VERTRES 200 350 480 600 760 768
HORZSIZE 240 240 208 208 280 208
VERTSIZE 180 175 156 152 210 152
ASPECTX 5 38 36 36 10 36
ASPECTY 12 48 36 36 14 36
ASPECTXY 13 61 51 51 14 51
LOGPIXELSX 96 96 96 96 120 96
LOGPIXELSY 48 72 96 96 120 96

Из этой таблицы видны недостатки физической системы координат.

Во-первых, вертикальное (VERTRES) и горизонтальное (HORZRES) разрешение зависит от типа видеоконтроллера.

Во-вторых, физические размеры пикселов (ASPECTX и ASPECTY ), и, что самое главное, отношение высоты и ширины пиксела также зависят от типа видеоконтроллера.

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

Логическая система координат

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

Рис. 2.2. Одна из возможных систем координат

Для установки режима отображения, непосредственно определяющего направление осей и размер логической единицы системы координат, используется функция SetMapMode :

int WINAPI SetMapMode(HDC hdc, int nMapMode);

Для контекста отображения hdc эта функция устанавливает новый режим отображения, заданный параметром nMapMode, возвращая номер режима отображения, который был установлен раньше.

Параметр nMapMode может принимать одно из следующих значений.

Режим отображения Направление оси X Направление оси Y Размер одной логической единицы
MM_TEXT Вправо Вниз 1 пиксел
MM_LOMETRIC Вправо Вверх 0,1 мм
MM_HIMETRIC Вправо Вверх 0,01 мм
MM_LOENGLISH Вправо Вверх 0,01 дюйм
MM_HIENGLISH Вправо Вверх 0,001 дюйм
MM_TWIPS Вправо Вверх 1/1440 дюйма
MM_ISOTROPIC Можно выбирать Можно выбирать Произвольный, одинаковый для осей X и Y
MM_ANISOTROPIC Можно выбирать Можно выбирать Произвольный, может быть разный для осей X и Y

Как видно из этой таблицы, в режиме отображения MM_TEXT, выбранном в контекст отображения по умолчанию, используется нестандартное (для геометрии, математики и физики) направление оси Y - вниз от начала координат. Мы уже говорили, что такое направление оси Y удобно для отображения текста, поэтому этот режим отображения иногда называют текстовым.

Нетрудно заметить, что в режиме MM_TEXT логическая единица длины полностью соответствует физической, поэтому при рисовании геометрических фигур возможны искажения формы. Эти искажения связаны с тем, что форма пиксела для некоторых видеоконтроллеров может быть отличной от квадратной. Режим MM_TEXT неудобен для рисования фигур.

В режимах MM_LOMETRIC, MM_HIMETRIC, MM_LOENGLISH, MM_HIENGLISH, MM_TWIPS используется более привычное направление осей координат и единицы длины, не зависящие от аппаратного обеспечения устройства вывода.

В режиме MM_ISOTROPIC вы можете выбирать произвольное направление осей координат и произвольный (но одинаковый) масштаб для осей X и Y. Заметим, что произвольное направление координат в нашем случае не подразумевает их произвольного расположения относительно вертикальной и горизонтальной осей - ось X может располагаться только горизонтально, ось Y - только вертикально.

Режим MM_ANISOTROPIC еще более универсален. Он позволяет устанавливать произвольное направление осей координат, произвольный масштаб для осей координат, причем для каждой оси можно установить свой собственный масштаб.

Во всех режимах отображения, кроме MM_TEXT и MM_ANISOTROPIC с разным масштабом для осей X и Y, приложение может не заботиться о "квадратуре пиксела", так как масштаб по осям координат одинаковый и не зависит от особенностей устройства вывода.

Сделаем небольшое пояснение относительно режима отображения MM_TWIPS. В этом режиме используется единица длины twip (от twentieth of a point - двадцатая часть точки, или, в терминах полиграфии, двадцатая часть пункта). Размер одного пункта приблизительно равен 1/72 дюйма, следовательно размер единицы длины twip равен 1/1440 дюйма.

С помощью функции GetMapMode приложение может в любой момент времени определить номер режима отображения, выбранный в контекст отображения hdc:

int WINAPI GetMapMode(HDC hdc);

Преобразование координат

Теперь немного математики (не волнуйтесь, совсем немного).

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

В этих формулах используются следующие обозначения.

Переменные xWindow и yWindow обозначают, соответственно, логические координаты по оси X и Y. Физические координаты обозначаются как xViewport и yViewport. Таким образом, логические координаты (xWindow,yWindow) преобразуются в физические координаты (xViewport,yViewport).

С помощью переменных xViewOrg и yViewOrg можно изменить расположение начала физических координат. По умолчанию в контексте отображения значения атрибутов, соответствующих этим переменным, равны 0. Приложение может сместить начало координат, изменив значения переменных xViewOrg и yViewOrg.

Для перемещения начала логической системы координат приложение может изменить значения переменных xWinOrg и yWinOrg, которые также определены как атрибуты контекста отображения. По умолчанию в этих переменных находятся нулевые значения.

Переменные xViewExt, yViewExt, xWinExt и yWinExt (вернее, их отношения) задают масштаб, который используется в процессе преобразования координат. Этот масштаб зависит от установленного режима отображения. Приложения могут изменить его только в режимах MM_ISOTROPIC и MM_ANISOTROPIC, для остальных режимов отображения используются фиксированные значения.

Обратные преобразования (из логических координат в физические) выполняются с использованием следующих формул:

Для выполнения подобных вычислений удобна функция MulDiv, определенная в программном интерфейсе Windows:

int WINAPI MulDiv(
  int nMultiplicand, // множимое
  int nMultiplier,   // множитель
  int nDivisor);     // делитель

Эта функция выполняет умножение 16-разрядного параметра nMultiplicand на 16-разрядный параметр nMultiplier, а затем делит полученное 32-разрядное произведение на 16-разрядный параметр nDivisor. В случае переполнения или при нулевом значении делителя функция возвращает отрицательное значение ­32768.

Однако есть специальные функции, предназначенные для преобразования логических координат в физические и физических в логические. Это функции LPtoDP и DPtoLP.

Функция LPtoDP выполняет преобразование логических координат в физические, причем одновременно можно преобразовать несколько пар координат, что ускоряет работу приложения за счет сокращения количества вызовов функции:

BOOL WINAPI LPtoDP(
  HDC hdc,         // идентификатор контекста отображения
  POINT FAR* lppt, // указатель на массив структур POINT
  int cPoints);    // размер массива структур POINT

Параметр hdc указывает контекст отображения, для которого требуется выполнить преобразования. В процессе преобразования используются атрибуты контекста, такие как смещение и масштаб осей координат.

Через параметр lppt передается указатель на массив структур POINT, в котором находятся преобразуемые координаты. Размер массива определяется значением параметра cPoints. Структура POINT определена в файле windows.h следующим образом:

typedef struct tagPOINT
{
   int x;
   int y;
} POINT;

После успешного выполнения преобразования функция возвращает значение TRUE. При возникновении ошибки возвращается FALSE.

Обратное преобразование (физических координат в логические ) выполняется функцией DPtoLP :

BOOL WINAPI DPtoLP(
  HDC hdc,         // идентификатор контекста отображения
  POINT FAR* lppt, // указатель на массив структур POINT
  int cPoints);    // размер массива структур POINT

Назначение параметров функции DPtoLP и возвращаемое ей значение аналогично назначению параметров и возвращаемому значению функции LPtoDP.

Есть еще две функции, предназначенные для преобразования координат - ClientToScreen и ScreenToClient.

Функция ClientToScreen выполняет преобразование координат в системе координат, связанной с внутренней областью окна, в экранные координаты:

void WINAPI ClientToScreen(HWND hwnd, POINT FAR* lppt);

Параметр hwnd содержит идентификатор окна, для которого выполняется преобразование.

Параметр lppt содержит указатель на структуру типа POINT, в которую перед вызовом функции следует записать преобразуемые координаты.

Функция ScreenToClient имеет аналогичные параметры, но выполняет обратное преобразование:

void WINAPI ScreenToClient(HWND hwnd, POINT FAR* lppt);

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

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

Режим MM_TEXT

Режим отображения MM_TEXT устанавливается в контексте отображения по умолчанию. Для этого режима формулы преобразования координат упрощаются:

xViewport = (xWindow - xWinOrg) + xViewOrg
yViewport = (yWindow - yWinOrg) + yViewOrg

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

Рис. 2.3. Система координат в режиме отображения MM_TEXT

Так как в формуле преобразования не присутствуют переменные xViewExt, yViewExt, xWinExt и yWinExt, в данном режиме преобразования невозможно изменить масштаб осей координат. Поэтому логическая единица длины в режиме отображения MM_TEXT равна физической, т. е. одному пикселу.

Тем не менее, приложение может изменить смещение физической или логической системы координат, изменив, соответственно, значение пар переменных (xViewOrg, yViewOrg) и (xWinOrg,yWinOrg). Для установки смещения можно использовать функции SetViewportOrg и SetWindowOrg.

Функция SetViewportOrg устанавливает смещение физической системы координат:

DWORD WINAPI SetViewportOrg(
  HDC hdc,       // контекст отображения
  int nXOrigin,  // новое значение для xViewOrg
  int nYOrigin); // новое значение для yViewOrg

Для контекста отображения hdc эта функция устанавливает новое расположение начала физической системы координат. Младшее и старшее слово возвращаемого значения содержат, соответственно, предыдущие x- и y-координаты начала физической системы координат.

С помощью функции SetWindowOrg вы можете установить начало логической системы координат:

DWORD WINAPI SetWindowOrg(
  HDC hdc,       // контекст отображения
  int nXOrigin,  // новое значение для xWinOrg
  int nYOrigin); // новое значение для yWinOrg

По своему смыслу параметры nXOrigin и nYOrigin являются логическими x- и y-координатами верхнего угла окна, используемого для отображения (так как в формуле они используются со знаком минус).

Как правило, приложения изменяют либо начало физических координат, вызывая функцию SetViewportOrg, либо начало логических координат, вызывая функцию SetWindowOrg, хотя, в принципе, вы можете изменить и то, и другое (если не боитесь запутаться в координатных "сетях").

В программном интерфейсе Windows версии 3.1 есть новые варианты описанных выше двух функций, которые называются SetViewportOrgEx и SetWindowOrgEx. Они отличаются более удобным способом передачи старых координат начала соответствующей системы координат:

BOOL WINAPI SetViewportOrgEx(
  HDC hdc,          // контекст отображения
  int nXOrigin,     // новое значение для xWinOrg
  int nYOrigin,     // новое значение для yWinOrg
  POINT FAR* lppt); // указатель на структуру POINT

BOOL WINAPI SetWindowOrgEx(
  HDC hdc,          // контекст отображения
  int nXOrigin,     // новое значение для xWinOrg
  int nYOrigin,     // новое значение для yWinOrg
  POINT FAR* lppt); // указатель на структуру POINT

В структуру, адрес которой передается через параметр lppt, записываются старые координаты начала системы координат. Обе функции возвращают TRUE в случае успеха и FALSE при возникновении ошибки.

В любой момент времени вы можете определить расположение начала физических или логических координат, если воспользуетесь функциями GetViewportOrg и GetWindowOrg (или их более новыми аналогами - GetViewportOrgEx и GetWindowOrgEx).

Функция GetViewportOrg возвращает x- и y-координаты начала физической системы координат для контекста отображения hdc:

DWORD WINAPI GetViewportOrg(HDC hdc);

Младшее и старшее слово возвращаемого значения содержат, соответственно, предыдущие x- и y-координаты начала физической системы координат.

Функция GetWindowOrg возвращает x- и y-координаты начала логической системы координат:

DWORD WINAPI GetWindowOrg(HDC hdc);

Новые функции, появившиеся в Windows версии 3.1, с именами GetViewportOrgEx и GetWindowExtEx записывают координаты начала координат в структуру, адрес которой передается через параметры lppt и lpSize:

BOOL WINAPI GetViewportOrgEx(HDC hdc, POINT FAR* lppt);
BOOL WINAPI GetWindowExtEx(HDC hdc, SIZE FAR* lpSize);

Структура SIZE определена для Windows версии 3.1 и описана в файле windows.h следующим образом:

typedef struct tagSIZE
{
  int cx;
  int cy;
} SIZE;

Определены также разнообразные указатели на структуру SIZE:

typedef SIZE*       PSIZE;
typedef SIZE NEAR* NPSIZE;
typedef SIZE FAR*  LPSIZE;

Метрические режимы отображения

Режим MM_LOMETRIC, наряду с режимами MM_HIMETRIC, MM_LOENGLISH, MM_HIENGLISH и MM_TWIPS, относится к метрическим режимам. Эти режимы отображения позволяют использовать привычные единицы измерения, такие как миллиметры и дюймы.

В метрических режимах отображения используются полные формулы преобразования координат, приведенные выше в разделе "Преобразование координат". В этих формулах приложение может изменять переменные, определяющие смещение начала физической или логической системы координат xViewOrg, yViewOrg, xWinOrg и yWinOrg.

Приложение не может изменить значения переменных xViewExt, yViewExt, xWinExt и yWinExt, от которых зависит масштаб по осям координат. Отношения xViewExt/xWinExt и yViewExt/yWinExt имеют фиксированное значение для каждого из метрических режимов отображения.

Заметим, что для этих режимов отношение yViewExt/yWinExt имеет отрицательный знак, в результате чего ось Y оказывается направленной снизу вверх.

Обратим ваше внимание на одно важное обстоятельство, связанное с использованием метрических режимов отображения.

Сразу после переключения в метрический режим отображения система координат примет достаточно странный вид (рис. 2.4).

Рис. 2.4. Ориентация осей сразу после переключения в метрический режим отображения

Ось X, как и следовало ожидать, окажется направленной слева направо, а ось Y - снизу вверх. Точка с координатами (0,0) будет находиться в верхнем левом углу экрана, поэтому для того чтобы нарисовать что-нибудь в такой системе координат, вам придется для y-координаты графических объектов использовать отрицательные числа. Для того чтобы система координат приняла более удобный вид, можно переместить начало физических координат в нижний левый угол окна или в центр окна.

Прежде, чем выполнять перемещение начала координат, следует определить размеры внутренней области окна. Это можно сделать при обработке сообщения WM_SIZE :

static short cxClient, cyClient;
....
case WM_SIZE:
{
  cxClient = LOWORD(lParam);
  cyClient = HIWORD(lParam);
 ....
  return 0;
} 

Для того чтобы расположить начало координат в левом нижнем углу окна, следует вызвать функцию SetViewportOrg, передав ей новые координаты начала физической системы координат (0,cyClient):

SetViewportOrg(hdc, 0, cyClient);

Полученная в результате система координат показана на рис. 2.5.

Рис. 2.5. Метрическая система координат, начало координат находится в левом нижнем углу окна

Аналогичным образом можно расположить начало системы координат в середине окна (рис. 2.6), обеспечив возможность использования положительных и отрицательных координат вдоль оси X и Y:

SetViewportOrg(hdc, cxClient/2, cyClient/2);

Рис. 2.6. Метрическая система координат, начало координат находится в центре окна

Режимы MM_ISOTROPIC и MM_ANISOTROPIC

Режимы отображения MM_ISOTROPIC (изотропный) и MM_ANISOTROPIC (анизотропный) допускают изменение направления осей X и Y, а также изменение масштаба осей координат. В изотропном режиме отображения MM_ISOTROPIC масштаб вдоль осей X и Y всегда одинаковый (т. е. для обоих осей используются одинаковые логические единицы длины). Анизотропный режим MM_ANISOTROPIC предполагает использование разных масштабов для разных осей (хотя можно использовать и одинаковые масштабы).

Для изменения ориентации и масштаба осей предназначены функции SetViewportExt, SetViewportExtEx, SetWindowExt и SetWindowExtEx.

Функция SetWindowExt устанавливает для формулы преобразования координат значения переменных xWinExt и yWinExt:

DWORD WINAPI SetWindowExt(
  HDC hdc,       // идентификатор контекста отображения
  int nXExtent,  // значение для xWinExt 
  int nYExtent); // значение для yWinExt

Функция SetViewportExt должна использоваться после функции SetWindowExt. Она устанавливает для формулы преобразования координат значения переменных xViewExt и yViewExt:

DWORD WINAPI SetViewportExt(
  HDC hdc,       // идентификатор контекста отображения
  int nXExtent,  // значение для xViewExt 
  int nYExtent); // значение для yViewExt

Обе функции возвращают в младшем и старшем слове предыдущие значения соответствующих переменных для оси X и Y.

Приведенные выше формулы можно использовать для установки отношений xViewExt/xWinExt и yViewExt/yWinExt, определяющих масштаб и направление осей координат (направление осей координат зависит от знака этих отношений).

Функции SetWindowExt передаются значения, соответствующие логическому размеру логического окна, в которое будет выполняться вывод, а функции SetViewportExt - реальные ширина и высота реального окна.

Например, нам надо создать систему координат, в которой начало отсчета расположено в левом нижнем углу окна, ось X направлена слева направо, а ось Y - снизу вверх. Высота и ширина должны изменяться от 0 до 32767 (максимально возможное значение, так как для координат используются 16-разрядные числа).

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

Приведем фрагмент кода, создающий необходимый режим отображения.

SetMapMode(hdc, MM_ISOTROPIC);
SetWindowExt(hdc, 32767, 32767);
SetViewportExt(hdc, cxClient, -cyClient);
SetViewportOrg(hdc, 0, cyClient);

В изотропном режиме отображения при изменении размеров окна Windows настроит систему координат таким образом, чтобы масштаб по осям X и Y был одинаковый.

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

Рис. 2.7. Изменение масштаба по горизонтали при увеличении ширины окна в изотропном режиме

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

Рис. 2.8. Изменение масштаба по горизонтали при увеличении высоты окна в изотропном режиме

При использовании анизотропного режима отображения MM_ANISOTROPIC настройка масштаба не выполняется, поэтому логическое окно будет занимать всю внутреннюю поверхность окна при любом изменении размеров этого окна (рис. 2.9 и 2.10).

Рис. 2.9. Изменение масштаба по горизонтали при увеличении ширины окна в анизотропном режиме

Рис. 2.10. Изменение масштаба по горизонтали при увеличении высоты окна в анизотропном режиме

В программном интерфейсе Windows версии 3.1 есть новые функции, предназначенные для изменения масштабов осей. Это функции SetViewportExtEx и SetWindowExtEx :

BOOL WINAPI SetViewportExtEx(
  HDC hdc,       // идентификатор контекста отображения
  int nXExtent,  // значение для xViewExt 
  int nYExtent,  // значение для yViewExt
  SIZE FAR* lpSize); // указатель на структуру SIZE

BOOL WINAPI SetWindowExtEx(
  HDC hdc,       // идентификатор контекста отображения
  int nXExtent,  // значение для xWinExt 
  int nYExtent,  // значение для yWinExt
  SIZE FAR* lpSize); // указатель на структуру SIZE

От функций SetViewportExt и SetWindowExt эти функции отличаются тем, что старые значения переменных, определяющих масштаб преобразования, записываются в структуру SIZE, указатель на которую передается через параметр lpSize.

Изотропный режим отображения удобно использовать в тех случаях, когда надо сохранить установленное отношение масштабов осей X и Y при любом изменении размеров окна, в которое выводится изображение (рис. 2.7 и 2.8).

Анизотропный режим удобен в тех случаях, когда изображение должно занимать всю внутреннюю поверхность окна при любом изменении размеров окна. Соотношение масштабов при этом не сохраняется (рис. 2.9 и 2.10).

2.3. Рисование геометрических фигур

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

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

С помощью функции GetDeviceCaps приложение может определить, поддерживает ли драйвер ту или иную функцию рисования.

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

Второй параметр iCapability определяет параметр устройства, значение которого необходимо получить.

Приведем список значений для второго параметра функции GetDeviceCaps, с помощью которых можно определить, какие операции рисования выполняет драйвер устройства вывода.

Имя константы Описание
LINECAPS Способности устройства рисовать линии. Возвращаемое значение представляет собой набор битовых масок, установленных в 1, если устройство может само рисовать линии различного типа:LC_INTERIORS устройство может закрашивать внутреннюю область;LC_MARKER маркеры;LC_NONE устройство не может рисовать линии;LC_POLYLINE ломаные линии;LC_POLYMARKER линии polymarker;LC_STYLED устройство может рисовать линии с использованием различных стилей (штриховые, пунктирные, штрих пунктирные и т.д.);LC_WIDE широкие линии;LC_WIDESTILED устройство может рисовать широкие линии с использованием различных стилей (штриховые, пунктирные, штрих-пунктирные и т. д.)
CURVECAPS Способность устройства рисовать различные кривые линии и геометрические фигуры. Возвращаемое значение представляет собой набор битовых масок, установленных в 1, если устройство может само рисовать различные фигуры:CC_CIRCLES окружности;CC_CHORD сегмент эллипса;CC_ELLIPSES эллипсы;CC_INTERIORS устройство может закрашивать внутреннюю область геометрических фигур;CC_NONE устройство не может рисовать кривые линии и геометрические фигуры;CC_PIE секторы эллипса;CC_ROUNDRECT прямоугольники со скругленными углами;CC_STYLED устройство может рисовать рамки с использованием различных стилей (штриховые, пунктирные, штрих-пунктирные и т.д.);CC_WIDE широкие рамки;CC_WIDESTYLED устройство может рисовать широкие рамки с использованием различных стилей (штриховые, пунктирные, штрих-пунктирные и т. д.)
POLYGONALCAPS Способности устройства рисовать многоугольники. Возвращаемое значение представляет собой набор битовых масок, установленных в 1, если устройство может само рисовать многоугольники различного типа:PC_INTERIORS устройство может закрашивать внутреннюю область;PC_NONE устройство не может рисовать многоугольники;PC_RECTANGLE прямоугольники;PC_SCANLINES устройство может выполнять сканирование линий растра;PC_STYLED устройство может рисовать рамки с использованием различных стилей (штриховые, пунктирные, штрих-пунктирные и т. д.);PC_WIDE широкие рамки;PC_WIDESTILED устройство может рисовать широкие рамки с использованием различных стилей (штриховые, пунктирные, штрих-пунктирные и т. д.)PC_WINDPOLYGON многоугольники с заполнением в режиме WINDING

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

Учитывая сказанное выше, не следует строить работу приложений таким образом, чтобы периодичность вывода или скорость работы приложения зависела от скорости рисования (подобная практика не приветствуется и при создании программ для MS-DOS, вспомните, как ведут себя старые игры, разработанные для процессора 8088, на компьютерах с процессорами i80386 или i486). Современные видеоадаптеры сконструированы таким образом, что большинство основных операций рисования, используемых в операционной системе Windows, выполняются аппаратно. Эти видеоадаптеры иногда называются ускорителями Windows. Скорость рисования для ускорителя Windows может превышать в десятки раз скорость рисования для обычного адаптера VGA или SVGA.

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

Работа с цветовыми палитрами и битовыми изображениями будут рассмотрены позже в отдельных разделах, так как эти вопросы далеко не тривиальны и поэтому заслуживают отдельного обсуждения.

Итак, перейдем непосредственно к описанию функций рисования геометрических фигур.

Рисование точки

Функция рисования точки SetPixel устанавливает цвет точки с заданными координатами:

COLORREF WINAPI SetPixel(
  HDC hdc,          // контекст отображения
  int nXPos,        // x-координата точки
  int nYPos,        // y-координата точки
  COLORREF clrref); // цвет точки

Установка первых трех параметров этой функции не должна вызывать у вас никаких затруднений. Параметр hdc определяет контекст отображения, для которого необходимо изменить цвет точки. Параметры nXPos и nYPos определяют координаты точки в системе координат, которая зависит от установленного для контекста hdc режима отображения.

Последний параметр clrref определяет новый цвет точки. О том, как "раскрасить" окно приложения, вы узнаете из третьей главы нашей книги. Тем не менее мы опишем самый простой способ для функции SetPixel.

В файле windows.h есть описание макрокоманды RGB, позволяющей сконструировать цвет в формате COLORREF из отдельных компонент красного (r), зеленого (g) и голубого (b) цвета:

#define RGB(r,g,b) \
   ((COLORREF)(((BYTE)(r)|((WORD)(g)<<8)) | \
   (((DWORD)(BYTE)(b))<<16)))

Вы можете использовать эту макрокоманду совместно с функцией SetPixel для установки, например, красного цвета точки, расположенной в начале системы координат (0,0), следующим образом:

SetPixel(hdc, 0, 0, RGB(0xff, 0, 0));

Три параметра макрокоманды RGB позволяют задать любой из примерно 16 млн. цветов и оттенков, однако это не означает, что вы получите на экране точно такой цвет, какой был задан при помощи этой макрокоманды. Скорее всего вы сможете воспользоваться одним из 20 системных цветов. Причины этого вы узнаете в третьей главе. Там же мы расскажем о том, как в некоторых случаях можно расширить используемую гамму цветов.

Функция SetPixel возвращает цвет, который фактически был использован для рисования точки. Как мы только что заметили, возвращенное значение может отличаться от заданного параметром clrref. В случае ошибки оно будет равно -1.

Функция GetPixel позволяет узнать цвет точки, заданной идентификатором контекста отображения и координатами:

COLORREF WINAPI GetPixel(HDC hdc, int nXPos, int nYPos);

С помощью следующих трех макрокоманд, определенных в файле windows.h, вы можете определить отдельные цветовые компоненты для значения, возвращаемого функциями SetPixel и GetPixel:

#define GetRValue (rgb) ((BYTE)(rgb))
#define GetGValue (rgb) ((BYTE)(((WORD)(rgb)) >> 8))
#define GetBValue (rgb) ((BYTE)((rgb)>>16))

Функции SetPixel и GetPixel используются достаточно редко, так как для построения графических изображений есть более мощные функции.

Рисование линий

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

Текущая позиция пера

Для рисования прямых линий (и только для этого) в контексте отображения хранятся координаты текущей позиции пера. Для изменения текущей позиции пера в Windows версии 3.1 есть две функции с именами MoveTo и MoveToEx. Для совместимости с 32-разрядными версиями Windows, такими, как Windows NT, в новых приложениях следует использовать функцию MoveToEx:

BOOL WINAPI MoveToEx(
  HDC hdc,          // идентификатор контекста отображения
  int x,            // x-координата
  int y,            // y-координата
  POINT FAR* lppt); // указатель на структуру POINT

Для контекста отображения hdc эта функция устанавливает текущую позицию пера, равную (x,y). В структуру типа POINT, на которую указывает параметр lppt, после возврата из функции будут записаны старые координаты пера.

Функция MoveToEx возвращает TRUE при нормальном завершении или FALSE при ошибке.

Чтобы узнать текущую позицию пера, приложение может использовать функцию GetCurrentPositionEx :

BOOL WINAPI GetCurrentPositionEx(HDC hdc, POINT FAR* lppt);

После вызова этой функции текущая позиция пера будет записана в структуру типа POINT, на которую указывает параметр lppt. Функция GetCurrentPositionEx возвращает TRUE при нормальном завершении или FALSE при ошибке.

Рисование прямой линии

Для того чтобы нарисовать прямую линию, приложение должно воспользоваться функцией LineTo :

BOOL WINAPI LineTo(HDC hdc, int xEnd, int yEnd);

Эта функция рисует линию из текущей позиции пера, установленной ранее функцией MoveTo или MoveToEx, в точку с координатами (xEnd,yEnd). После того как линия будет нарисована, текущая позиция пера станет равной (xEnd,yEnd).

Функция LineTo возвращает TRUE при нормальном завершении или FALSE при ошибке.

Таким образом, для того чтобы нарисовать прямую линию, приложение должно сначала с помощью функции MoveToEx установить текущую позицию пера в точку, которая будет началом линии, а затем вызвать функцию LineTo, передав ей через параметры xEnd и yEnd координаты конца линии.

Особенностью функции LineTo является то, что она немного не дорисовывает линию - эта функция рисует всю линию, не включая ее конец, т. е. точку (xEnd,yEnd). Это иллюстрируется на рис. 2.11.

Рис. 2.11. Рисование прямой линии

Если вас не устраивает необходимость пользоваться двумя функциями для рисования линии, вы можете создать свою собственную, например такую:

BOOL DrawLine(HDC hdc, int x1, int y1, int x2, int y1)
{
  POINT pt;
  MoveToEx(hdc, x1, y1, &pt);
  return LineTo(hdc, x2, y2);
}

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

Рисование ломаной линии

Функции Polyline, предназначенной для рисования ломаных линий, следует передать идентификатор контекста отображения hdc, указатель lppt на массив структур POINT, в котором должны находится координаты начала ломаной линии, координаты точек излома и координаты конца ломаной линии, а также размер этого массива cPoints:

BOOL WINAPI Polyline(
  HDC hdc,             // идентификатор контекста отображения
  const POINT FAR* lppt,// указатель на массив структур POINT
  int cPoints);         // размер массива

Функция Polyline возвращает TRUE при нормальном завершении или FALSE при ошибке. Она не использует текущую позицию пера и не изменяет ее.

Если ломаная линия не замкнута, ее последняя точка не рисуется (рис. 2.12).

Рис. 2.12. Рисование ломаной линии

Рисование дуги эллипса

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

BOOL WINAPI Arc(
  HDC hdc,   // идентификатор контекста отображения
  int nxLeft,  int nyTop,    // верхий левый угол
  int nxRight, int nyBottom, // правый нижний угол
  int nxStart, int nyStart,  // начало дуги
  int nxEnd,   int nyEnd);   // конец дуги

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

Рис. 2.13. Рисование дуги эллипса

Параметры (nxLeft,nyTop) и (nxRight,nyBottom) задают координаты, соответственно, верхнего левого и правого нижнего углов воображаемого прямоугольника, в который вписан эллипс.

Начало дуги эллипса определяется пересечением эллипса с воображаемой прямой линией, проведенной из центра эллипса (xC,yC) в точку (xStart,yStart).

Конец дуги определяется аналогично - как пересечение эллипса с воображаемой прямой линии, проведенной из центра эллипса в точку (xEnd,yEnd).

Дуга рисуется в направлении против часовой стрелки.

Координаты центра эллипса (если это потребуется) можно вычислить следующим образом:

xC = (nxLeft + nxRight) / 2;
yC = (nyTop + nyBottom) / 2;

Настройка атрибутов контекста отображения для рисования линий

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

Выбор пера

Для рисования линий приложения Windows могут выбрать одно из трех встроенных перьев, либо создать собственное перо.

Для выбора встроенного пера лучше всего воспользоваться макрокомандами GetStockPen и SelectPen, определенными в файле windowsx.h:

#define GetStockPen(i) ((HPEN)GetStockObject(i))
#define SelectPen(hdc, hpen) \
  ((HPEN)SelectObject((hdc), (HGDIOBJ)(HPEN)(hpen)))

Макрокоманда GetStockPen возвращает идентификатор встроенного пера, заданного параметром i. Вы можете выбрать для этого параметра одно из следующих значений:

Значение Описание
BLACK_PEN Перо, рисующее черную линию толщиной в один пиксел (для любого режима отображения). Это перо выбрано в контекст отображения по умолчанию
WHITE_PEN Перо белого цвета. Толщина пера также равна одному пикселу и не зависит от режима отображения
NULL_PEN Невидимое перо толщиной в один пиксел. Используется для рисования замкнутых закрашенных фигур (таких, как прямоугольник или эллипс) в тех случаях, когда контур фигуры должен быть невидимым

После получения идентификатора пера его необходимо выбрать в контекст отображения при помощи макрокоманды SelectPen. Первый параметр этой макрокоманды используется для указания идентификатора контекста отображения, в который нужно выбрать перо, второй - для передачи идентификатора пера.

Макрокоманда SelectPen возвращает идентификатор пера, который был выбран в контекст отображения раньше. Вы можете сохранить этот идентификатор и использовать его для восстановления старого пера.

Однако при помощи встроенных перьев вы не можете нарисовать цветные, широкие, штриховые и штрих-пунктирные линии.

Если вас не устраивают встроенные перья, вы можете легко создать собственные. Для этого нужно воспользоваться функциями CreatePen или CreatePenIndirect.

Функция CreatePen позволяет определить стиль, ширину и цвет пера:

HPEN WINAPI CreatePen(
  int fnPenStyle,   // стиль пера
  int nWidth,       // ширина пера
  COLORREF clrref); // цвет пера

Параметр fnPenStyle определяет стиль линии и может принимать одно из следующих значений, определенных в файле windows.h:

Стиль линии Внешний вид Описание
PS_SOLID Сплошная
PS_DASH Штриховая
PS_DOT Пунктирная
PS_DASHDOT Штрих-пунктирная, одна точка на одну линию
PS_DASHDOTDOT Штрих-пунктирная, две точки на одну линию
PS_NULL   Невидимая
PS_INSIDEFRAME Линия, предназначенная для обводки замкнутых фигур

Параметр nWidth определяет ширину пера. Используемая при этом единица длины зависит от режима отображения, поэтому вы можете задавать ширину пера не только в пикселах, но и в долях миллиметра или дюйма. Учтите, что для линий PS_DASH, PS_DOT, PS_DASHDOT, PS_DASHDOTDOT можно использовать только единичную или нулевую ширину, в обоих случаях ширина линии будет равна одному пикселу. Поэтому вы не можете создать перо для рисования, например, пунктирной линии шириной 5 миллиметров.

Параметр clrref задает цвет пера.

На первый взгляд линии PS_SOLID и PS_INSIDEFRAME похожи, однако между ними имеются различия, особенно заметные для широких линий. Широкая линия, имеющая стиль PS_SOLID, располагается по обе стороны оси, заданной координатами линии. Линии, имеющие стиль PS_INSIDEFRAME, располагаются внутри контура, определяющего размеры замкнутой фигуры (рис. 2.14).

Рис. 2.14. Использование стилей PS_SOLID и PS_INSIDEFRAME

Еще одно отличие связано с использованием смешанных цветов (dithered color). Когда Windows не может в точности подобрать цвет, указанный для толстой линии стиля PS_INSIDEFRAME, он раскрашивает эту линию с использованием смешанного цвета, полученного из других цветов. В этом случае изображение линии формируется из отдельных точек разных цветов. Техника смешанных цветов может применяться и при закрашивании замкнутых фигур, о чем мы еще будем говорить.

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

Небольшое замечание относительно концов толстых линий. Концы толстых линий закруглены (рис. 2.15).

Рис. 2.15. Закругленные концы толстой линии

Для изображения толстой линии с прямыми концами следует задать прямоугольную область ограничения (см. ниже раздел, посвященный области ограничения). Можно также нарисовать толстую линию как закрашенный прямоугольник с использованием тонкого пера.

Другая возможность создать перо - вызвать функцию CreatePenIndirect :

HPEN WINAPI CreatePenIndirect(LOGPEN FAR* lplgpn);

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

Структура LOGPEN и различные указатели на нее определены в файле windows.h:

typedef struct tagLOGPEN
{
  UINT    lopnStyle;   // стиль пера
  POINT   lopnWidth;   // ширина пера
  COLORREF lopnColor;  // цвет пера
} LOGPEN;
typedef LOGPEN*       PLOGPEN;
typedef LOGPEN NEAR* NPLOGPEN;
typedef LOGPEN FAR*  LPLOGPEN;

Заметим, что ширина пера в данном случае находится в поле x структуры POINT. Поле y не используется.

Если вы создали перо, его можно выбрать в контекст отображения при помощи макрокоманды SelectPen. После этого можно рисовать линии обычным образом, вызывая функции MoveToEx и LineTo.

Созданные вашим приложением перья принадлежат GDI, соответствующие структуры данных располагаются в его сегменте данных. Поэтому если перо больше не нужно, его нужно удалить для освобождения памяти.

Прежде чем удалять созданное вами перо, следует выбрать в контекст отображения одно из встроенных перьев (например то, которое использовалось раньше). После этого для удаления вашего пера нужно вызвать макрокоманду DeleletePen, определенную в файле windowsx.h:

#define DeletePen(hpen) DeleteObject((HGDIOBJ)(HPEN)(hpen))

В качестве параметра этой макрокоманде необходимо передать идентификатор удаляемого пера.

Нельзя удалять перо, если оно выбрано в контекст отображения. Нет никакого смысла в удалении встроенных перьев.

Выбор режима фона

Режим фона влияет на заполнение промежутков между штрихами и точками в штрих-пунктирных, штриховых и пунктирных линиях.

Напомним, что по умолчанию в контексте отображения установлен непрозрачный режим фона OPAQUE. В этом режиме промежутки закрашиваются цветом фона, определенным как атрибут контекста отображения. Приложение может установить прозрачный режим фона TRANSPARENT, в этом случае промежутки в линиях не будут закрашиваться (рис. 2.16).

Рис. 2.16. Режимы фона OPAQUE и TRANSPARENT

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

int WINAPI SetBkMode(HDC hdc, int fnBkMode);

Эта функция устанавливает новый режим фона fnBkMode для контекста отображения hdc, возвращая в случае успеха код старого режима фона.

Для параметра fnBkMode вы можете использовать значения OPAQUE или TRANSPARENT, определенные в файле windows.h.

Приложение может определить текущий режим фона, вызвав функцию GetBkMode :

int WINAPI GetBkMode(HDC hdc);

С помощью функций SetBkColor и GetBkColor вы можете, соответственно, установить и определить текущий цвет фона, который используется для закраски промежутков между штрихами и точками линий:

COLORREF WINAPI SetBkColor(HDC hdc, COLORREF clrref);
COLORREF WINAPI GetBkColor(HDC hdc);

Выбор режима рисования

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

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

Для выбора режима рисования предназначена функция SetROP2 :

int WINAPI SetROP2(HDC hdc, int fnDrawMode);

Параметр hdc предназначен для указания контекста отображения, в котором необходимо установить новый режим рисования, определяемый параметром fnDrawMode.

Функция SetROP2 возвращает код предыдущего режима рисования.

Процесс рисования на экране монитора заключается в выполнении логической операции над цветами точек экрана и цветами изображения. Ниже в таблице мы привели возможные значения для параметра fnDrawMode. Для каждого режима рисования в этой таблице есть формула, с использованием которой вычисляется результат, и краткое описание режима рисования. В формулах цвет пера обозначается буквой P, цвет подложки - D.

Режим рисования Формула Цвет пиксела
R2_COPYPEN P Соответствует (равен) цвету пера
R2_BLACK 0 Черный
R2_WHITE 1 Белый
R2_NOP D Не меняется, т. е. перо ничего не рисует
R2_NOT ~D Получается инвертированием цвета подложки, т. е. цвета пиксела до рисования
R2_NOTCOPYPEN ~P Получается инвертированием цвета пера
R2_MASKPEN P&D Комбинация компонент цветов, имеющихся как в цвете подложки, так и в цвете пера
R2_NOTMASKPEN ~(P&D) Инверсия предыдущего значения
R2_MERGEPEN P|D Комбинация компонент цветов подложки и пера
R2_NOTMERGEPEN ~(P|D) Инверсия предыдущего значения
R2_XORPEN P^D При определении цвета пиксела выполняется операция "ИСКЛЮЧАЮЩЕЕ ИЛИ" между компонентами цвета подложки и пера
R2_NOTXORPEN ~(P^D) Инверсия предыдущего значения
R2_MASKNOTPEN ~P & D Комбинация цвета подложки и инверсии цвета пера
R2_MASKPENNOT P & ~D Комбинация двух цветов: инверсии цвета подложки и цвета пера
R2_MERGENOTPEN ~P | D Комбинация компонент цветов подложки и инверсии цвета пера
R2_MERGEPENNOT P | ~D Комбинация инверсии цвета подложки и цвета пера

Если изображение и перо черно-белые, результат выполнения описанных выше операций (которые, кстати, называются растровыми операциями ) можно легко предсказать.

В режиме R2_COPYPEN, который установлен в контексте отображения по умолчанию, цвет нарисованной линии будет такой же, как и цвет пера. Для режимов R2_BLACK и R2_WHITE цвет линии будет, соответственно, черный и белый. В режиме R2_NOP вы не увидите нарисованную линию, так как цвет вдоль нее вообще не изменится. Более интересен режим R2_NOT, при использовании которого на черном фоне будет нарисована белая линия, а на белом фоне - черная.

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

С помощью функции GetROP2 приложение может определить режим рисования, установленный для контекста отображения hdc:

int WINAPI GetROP2(HDC hdc);

Рисование линий произвольного стиля

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

В программном интерфейсе GDI есть функция с именем LineDDA, которая позволяет рисовать любые линии (правда, основная работа по рисованию линий при этом будет возложена на программиста).

Функция LineDDA имеет следующий прототип:

void WINAPI LineDDA(
  int nxStart, int nyStart, // начальная точка
  int nxEnd, int nyEnd,     // конечная точка
  LINEDDAPROC lnddaprc,     // адрес функции для рисования
  LPARAM lParam);           // дополнительные параметры

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

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

Для режима STRICT тип LINEDDAPROC определен в файле windows.h следующим образом:

typedef void (CALLBACK* LINEDDAPROC)(int, int, LPARAM);

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

Приведем прототип функции рисования (для функции можно использовать любое имя):

void CALLBACK _export
LineProc(int xPos, int yPos, LPARAM lParam);

Первые два параметра представляют собой координаты точки, для рисования которых вызвана функция. Последний параметр соответствует последнему параметру функции LineDDA и содержит передаваемое этой функции значение.

Пример использования функции LineDDA вы можете найти ниже в разделе "Приложение DASHLINE".

Рисование замкнутых фигур

Помимо линий, приложения Windows могут использовать функции GDI для рисования замкнутых закрашенных или незакрашенных фигур, таких как прямоугольники, эллипсы, многоугольники с прямыми и скругленными углами и т. д.

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

Мы начнем изучение функций GDI, предназначенных для рисования замкнутых фигур, с функций рисования прямоугольников.

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

Простейшая функция, с помощью которой можно нарисовать прямоугольник, называется Rectangle :

BOOL WINAPI Rectangle(
  HDC hdc,   // идентификатор контекста отображения
  int nxTL,  // координата x верхнего левого угла
  int nyTL,  // координата y верхнего левого угла
  int nxBR,  // координата x правого нижнего угла
  int nyBR); // координата y правого нижнего угла

Функция Rectangle рисует прямоугольник для контекста отображения hdc, возвращая значение TRUE в случае успеха или FALSE при ошибке.

Назначение остальных параметров иллюстрируется рис. 2.17.

Рис. 2.17. Рисование прямоугольника

Как видно из этого рисунка, последние четыре параметра функции задают координаты верхнего левого и нижнего правого угла прямоугольника.

В зависимости от стиля пера граница фигуры может находится полностью внутри прямоугольника, заданного координатами (nxTL, nyTL), (nxBR,nyBR) или выходить за его пределы (см. рис. 2.14). Если выбрать стиль пера PS_NULL, граница фигуры станет невидимой.

В зависимости от кисти, выбранной в контекст отображения, внутренность прямоугольника может быть закрашенной в тот или иной цвет, заштрихована одним из нескольких способов (как показано на рис. 2.16) или закрашена с помощью любого битового изображения размером 8х8 пикселов.

С помощью функции RoundRect можно нарисовать прямоугольник со скругленными углами (рис. 2.18).

Рис. 2.18. Прямоугольник со скругленными углами

По сравнению с функцией Rectangle функция RoundRect имеет два дополнительных параметра nxEllipse и nyEllipse, определяющих форму и радиус закругления:

BOOL WINAPI RoundRect(
  HDC hdc,   // идентификатор контекста отображения
  int nxTL,  // координата x верхнего левого угла
  int nyTL,  // координата y верхнего левого угла
  int nxBR,  // координата x правого нижнего угла
  int nyBR,  // координата y правого нижнего угла
  int nxEllipse,  // ширина эллипса
  int nyEllipse); // высота эллипса

Есть и другие функции, которые можно использовать для рисования прямоугольников.

Функция FillRect закрашивает прямоугольную область окна заданной кистью:

int WINAPI FillRect(
  HDC hdc,   // идентификатор контекста отображения
  const RECT FAR* lprc, // указатель на структуру RECT
  HBRUSH hbrush); // идентификатор кисти для закрашивания

Параметр lprc должен указывать на структуру типа RECT, в которую следует записать координаты закрашиваемой прямоугольной области. Правая и нижняя граница указанной области не закрашивается.

Независимо от того, какая кисть выбрана в контекст отображения, функция FillRect будет использовать для закраски кисть, указанную параметром hbrush.

Учтите, что правильная работа функции FillRect гарантируется только в том случае, когда значение поля bottom структуры RECT больше значения поля top, а значение поля right больше значения поля left.

Для закрашивания границы прямоугольной области (т. е. для рисования прямоугольной рамки) можно использовать функцию FrameRect :

int WINAPI FrameRect(
  HDC hdc,   // идентификатор контекста отображения
  const RECT FAR* lprc, // указатель на структуру RECT
  HBRUSH hbrush); // идентификатор кисти для закрашивания

Параметры этой функции аналогичны параметрам функции FillRect.

Ширина пера, используемого для рисования рамки, всегда равна одной логической единице. Структура RECT должна быть подготовлена таким же образом, что и для функции FillRect, т. е. значение поля bottom структуры RECT должно быть больше значения поля top, а значение поля right - больше значения поля left.

Значение, возвращаемое функциями FillRect и FrameRect не используется, приложения должны его игнорировать.

Используя функцию InvertRect, вы можете инвертировать содержимое прямоугольной области, заданной параметром lprc:

void WINAPI InvertRect(HDC hdc, const RECT FAR* lprc);

Есть еще одна интересная функция, предназначенная для рисования прямоугольников. Она имеет имя DrawFocusRect :

void WINAPI DrawFocusRect(HDC hdc, const RECT FAR* lprc);

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

Функция DrawFocusRect имеет три интересные особенности.

Во-первых, для рисования используется растровая операция "ИСКЛЮЧАЮЩЕЕ ИЛИ". Это приводит к тому, что для удаления нарисованной таким образом рамки ее достаточно нарисовать еще раз на том же месте.

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

Третья особенность заключается в том, что перед использованием этой функции необходимо установить режим отображения MM_TEXT.

Первые две особенности позволяют использовать ее для рисования рамки выделения произвольных участков изображения на экране монитора (при помощи мыши).

В заключение отметим, что в программном интерфейсе Windows нет функции для рисования квадрата и круга. Эти фигуры являются частными случаями, соответственно, прямоугольника и эллипса, поэтому для рисования, например, квадрата, вы должны использовать одну из только что описанных функций. Для сохранения пропорций проще всего использовать одну из метрических систем координат.

Рисование эллипса

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

BOOL WINAPI Ellipse(
  HDC hdc,   // идентификатор контекста отображения
  int nxTL,  // координата x верхнего левого угла
  int nyTL,  // координата y верхнего левого угла
  int nxBR,  // координата x правого нижнего угла
  int nyBR); // координата y правого нижнего угла

Первый параметр этой функции указывает идентификатор контекста отображения, остальные - координаты верхнего левого и правого нижнего углов прямоугольника, в который должен быть вписан эллипс (рис. 2.19).

Рис. 2.19. Рисование эллипса

Рисование сегмента эллипса

Сегмент эллипса (рис. 2.20) можно нарисовать при помощи функции Chord :

BOOL WINAPI Chord(
  HDC hdc,   // идентификатор контекста отображения
  int nxLeft,  int nyTop,    // верхий левый угол
  int nxRight, int nyBottom, // правый нижний угол
  int nxStart, int nyStart,  // начало дуги
  int nxEnd,   int nyEnd);   // конец дуги

Параметры этой функции аналогичны параметрам рассмотренной нами ранее функции Arc.

Рис. 2.20. Рисование сегмента эллипса

Рисование сектора эллипса

Для рисования сектора эллипса (рис. 2.21) следует использовать функцию Pie, аналогичную по своим параметрам функциям Arc и Chord:

BOOL WINAPI Pie(
  HDC hdc,   // идентификатор контекста отображения
  int nxLeft,  int nyTop,    // верхний левый угол
  int nxRight, int nyBottom, // правый нижний угол
  int nxStart, int nyStart,  // начало дуги
  int nxEnd,   int nyEnd);   // конец дуги

Рис. 2.21. Рисование сектора эллипса

Рисование многоугольников

Рисование многоугольников (рис. 2.22) выполняется функцией Polygon, аналогичной по своим параметрам функции Polyline, с помощью которой рисуются ломаные линии:

BOOL WINAPI Polygon(
  HDC hdc,             // идентификатор контекста отображения
  const POINT FAR* lppt,// указатель на массив структур POINT
  int cPoints);         // размер массива

Через параметр hdc передается идентификатор контекста отображения.

Параметр lppt указывает на массив структур POINT, в котором должны находится координаты вершин многоугольника. Параметр cPoints определяет размер этого массива.

Функция Polygon возвращает TRUE при нормальном завершении или FALSE при ошибке. Она не использует текущую позицию пера и не изменяет ее.

Рис. 2.22. Рисование многоугольника

В массиве структур POINT, определяющих вершины многоугольника, каждая вершина должна быть указана один раз. Функция Polygon автоматически замыкает ломаную линию, образующую многоугольник.

С помощью функции PolyPolygon можно нарисовать одновременно несколько многоугольников:

BOOL WINAPI PolyPolygon(
  HDC hdc,             // идентификатор контекста отображения
  const POINT FAR*lppt, // указатель на массив структур POINT
  int FAR* lpnPolyCounts, // адрес массива количества точек
                          //    в многоугольниках
  int cPolygons);         // количество многоугольников

Первый параметр hdc, как обычно, задает контекст отображения.

Параметр cPolygons определяет количество многоугольников, которые нужно нарисовать.

Параметр lppt должен содержать указатель на массив структур типа POINT, содержащий координаты вершин всех многоугольников.

И, наконец, через параметр lpnPolyCounts передается указатель на массив целых чисел. Каждое число в этом массиве определяет количество точек в соответствующем многоугольнике.

В отличие от функции Polygon, функция PolyPolygon не замыкает автоматически ломаную линию, образующую многоугольник.

В контексте отображения имеется атрибут, влияющий на способ закрашивания для самопересекающихся многоугольников. По умолчанию выбран режим ALTERNATE, в котором эти области не закрашиваются (закрашиваются только те области, которые расположены между нечетными и четными сторонами многоугольника).

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

Немного позже вы сможете изучить этот алгоритм с помощью приложения LINER.

Приведем прототип функции SetPolyFillMode:

int WINAPI SetPolyFillMode(HDC hdc, int fnMode);

Параметр fnMode, определяющий режим закрашивания многоугольников, может принимать значения ALTERNATE или WINDING. Функция возвращает код старого режима закрашивания.

Вы можете определить используемый в данный момент режим закрашивания многоугольников с помощью функции GetPolyFillMode :

int WINAPI GetPolyFillMode(HDC hdc);

Выбор кисти

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

Использование встроенной кисти

Для выбора одной из встроенной кисти вы можете воспользоваться макрокомандой GetStockBrush, определенной в файле windowsx.h:

#define GetStockBrush(i) ((HBRUSH)GetStockObject(i))

В качестве параметра для этой макрокоманды можно использовать следующие значения:

Значение Описание
BLACK_BRUSH Кисть черного цвета
WHITE_BRUSH Кисть белого цвета
GRAY_BRUSH Серая кисть
LTGRAY_BRUSH Светло-серая кисть
DKGRAY_BRUSH Темно-серая кисть
NULL_BRUSH Бесцветная кисть, которая ничего не закрашивает
HOLLOW_BRUSH Синоним для NULL_BRUSH

Как видно из только что приведенной таблицы, в Windows есть только монохромные встроенные кисти.

Макрокоманда GetStockBrush возвращает идентификатор встроенной кисти.

Прежде чем использовать полученную таким образом кисть, ее надо выбрать в контекст отображения (так же, как и перо). Для этого проще всего воспользоваться макрокомандой SelectBrush :

#define SelectBrush(hdc, hbr) \
   ((HBRUSH)SelectObject((hdc), (HGDIOBJ)(HBRUSH)(hbr)))

Макрокоманда SelectBrush возвращает идентификатор старой кисти, выбранной в контекст отображения раньше.

Создание кисти

Если вам нужна цветная кисть, ее следует создать с помощью функции CreateSolidBrush :

HBRUSH WINAPI CreateSolidBrush(COLORREF clrref);

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

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

После использования созданной вами кисти ее следует удалить, не забыв перед этим выбрать в контекст отображения старую кисть. Для удаления кисти следует использовать макрокоманду DeleteBrush :

#define DeleteBrush(hbr) DeleteObject((HGDIOBJ)(HBRUSH)(hbr))

Приложение может заштриховать внутреннюю область замкнутой фигуры, создав одну из шести кистей штриховки функцией CreateHatchBrush :

HBRUSH WINAPI CreateHatchBrush(int fnStyle, COLORREF clrref);

С помощью параметра clrref вы можете определить цвет линий штриховки.

Параметр fnStyle задает стиль штриховки:

Стиль штриховки Внешний вид
HS_BDIAGONAL
HS_CROSS
HS_DIAGCROSS
HS_FDIAGONAL
HS_HORIZONTAL
HS_VERTICAL

Вы можете использовать свой собственный стиль штриховки, создав кисть из битового изображения размером 8х8 пикселов (можно использовать только такой размер).

Если битовое изображение кисти определено в ресурсах приложения, его следует загрузить при помощи функции LoadBitmap. Эта функция возвратит идентификатор битового изображения. Затем для создания кисти этот идентификатор следует передать в качестве параметра функции CreatePatternBrush :

HBRUSH WINAPI CreatePatternBrush(HBITMAP hBitmap);

Забегая вперед, скажем, что битовые изображения делятся на те, которые хранятся в формате, зависящем от аппаратных особенностей устройства отображения, и на те, которые хранятся в аппаратно-независимом формате. Последние более универсальны, однако труднее в использовании. С помощью функции CreateDIBPatternBrush вы можете использовать для кисти битовое изображение в аппаратно-независимом формате:

HBRUSH WINAPI CreateDIBPatternBrush(
  HGLOBAL hglbDibPacked, UINT fnColorSpec);

Первый параметр указывает на область глобальной памяти, в которой содержится аппаратно-независимое битовое изображение в упакованном формате. Второй параметр определяет содержимое таблицы цветов, используемое этим битовым изображением, и может принимать два значения: DIB_PAL_COLORS (таблица цветов содержит ссылки на цветовую палитру) DIB_RGB_COLORS (таблица цветов содержит отдельные компоненты цвета).

Более подробное обсуждение таких понятий, как аппаратно-независимые битовые изображения и таблица цветов мы отложим до главы, посвященной битовым изображениям.

Закрашивание внутренней области окна

Напомним, что кисть можно использовать еще и для закрашивания внутренней области окна. Для этого идентификатор кисти следует записать в поле hbrBackground структуры типа WNDCLASS перед регистрацией класса окна:

wc.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH);

Установка начальных координат кисти

Начальные координаты кисти (brush origin ) - это атрибут контекста отображения. Он используются для определения координат точки внутри кисти, которая будет служить начальной при закраске внутренней области фигуры или окна. По умолчанию используются координаты (0,0), соответствующие верхнему левому углу кисти (в системе координат, выбранной в контекст отображения по умолчанию).

Если кисть используется для закраски внутренней области окна, верхний левый угол изображения кисти совмещается с верхним левым углом этой области. Затем изображение кисти многократно повторяется с шагом 8 пикселов.

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

Приложение может изменить начальные координаты кисти (сдвинуть кисть) при помощи функций SetBrushOrg и UnrealizeObject.

Прежде всего нужно вызвать функцию UnrealizeObject, передав ей в качестве параметра идентификатор сдвигаемой кисти (только если это не встроенная кисть):

BOOL WINAPI UnrealizeObject(HGDIOBJ hbrush);

В этом случае система сбросит координаты кисти после выбора ее в контекст отображения. После сброса надо установить новые значения координат кисти, вызвав функцию SetBrushOrg :

DWORD WINAPI SetBrushOrg(HDC hdc, int nx, int ny);

Параметры nx и ny определяют новые значения для начальных координат кисти пикселах (от 0 до 7).

В завершении следует снова выбрать кисть в контекст отображения при помощи макрокоманды SelectBrush.

2.4. Приложение LINER

Для демонстрации функций рисования графических изображений в различных режимах фона и с использованием различных растровых операций мы подготовили приложение LINER. Кроме всего прочего, в этом приложении мы получаем контекст отображения, пригодный для рисования во всем окне, в частности, в области заголовка окна (рис. 2.23).

Рис. 2.23. Приложение LINER

Меню "Draw" предназначено для выбора фигур, отображаемых в окне приложения. Вы можете выбрать прямые линии (строка "Lines"), ломаную линию (строка "Polyline"), дугу эллипса ("Arc"), прямоугольники ("Rectangles"), многоугольники ("Polygon"), эллипс ("Ellipse"), окружность ("Circle"), сегмент эллипса ("Chord") или сектор эллипса ("Pie").

Запустите приложение LINER (готовый загрузочный модуль и исходные тексты приложения есть на дискете, которую можно купить вместе с книгой) и выберите из меню "Draw" строку "Rectangles". В главном окне будет нарисовано несколько прямоугольников, в том числе один прямоугольник будет нарисован по периметру внутренней области окна.

По умолчанию сразу после запуска приложения используется режим фона OPAQUE. Смените его на TRANSPARENT, выбрав соответствующую строку из меню "Background Mode". Режим фона изменится на прозрачный (рис. 2.24).

Рис. 2.24. Прозрачный режим фона

Посмотрите, как отражается изменение фона на рисовании других фигур, например, прямых линий (рис. 2.25).

Рис. 2.25. Рисование прямых линий различной толщины и стиля

Меню "ROP" позволит вам провести эксперименты с различными растровыми операциями.

Выбрав из меню "Draw" строку "Polygon", вы сможете посмотреть, как закрашиваются самопересекающиеся многоугольники в режимах ALTERNATE и WINDING (рис. 2.26).

Рис. 2.26. Режимы закрашивания многоугольников

Главный файл приложения LINER приведен в листинге 2.1.

Листинг 2.1. Файл liner/liner.cpp

// ----------------------------------------
// Приложение LINER
// Демонстрация использования функций
// рисования графических изображений
// ----------------------------------------

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <mem.h>
#pragma hdrstop

#include "liner.hpp"

// Прототипы функций
BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);
void DrawLines(HDC);
void DrawPolyline(HDC);
void DrawArc(HDC);
void DrawRectangles(HDC);
void DrawPolygon(HDC);
void DrawEllipse(HDC);
void DrawCircle(HDC);
void DrawPie(HDC);
void DrawChord(HDC);

// Имя класса окна
char const szClassName[]   = "LinerClass";

// Заголовок окна
char const szWindowTitle[] = "Liner";

// Размеры внутренней области окна
short cxClient, cyClient;

// Код выбранной строки меню "Draw"
int nFigures = 0;

// Режим фона
int nBkMode = OPAQUE;

// Код растровой операции
int nROP2 = R2_COPYPEN;

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance, 
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine, 
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  // Инициализируем приложение
  if(!InitApp(hInstance))
      return FALSE;

  // После успешной инициализации приложения создаем
  // главное окно приложения
  hwnd = CreateWindow(
    szClassName,         // имя класса окна
    szWindowTitle,       // заголовок окна
    WS_OVERLAPPEDWINDOW, // стиль окна
    CW_USEDEFAULT,       // задаем размеры и расположение
    CW_USEDEFAULT,       // окна, принятые по умолчанию
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    0, 0, hInstance, NULL);      
                       
  // Если создать окно не удалось, завершаем приложение
  if(!hwnd)
    return FALSE;

  // Рисуем главное окно
  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// Выполняет регистрацию класса окна
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна

  // Записываем во все поля структуры нулевые значения
  memset(&wc, 0, sizeof(wc));

  // Подключаем меню 
  wc.lpszMenuName  = "APP_MENU";

  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH);
  wc.lpszClassName = (LPSTR)szClassName;

  // Регистрация класса
  aWndClass = RegisterClass(&wc);

  return (aWndClass != 0);
}

// =====================================
// Функция WndProc
// =====================================

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;

  switch (msg)
  {
    // При изменении размеров окна сохраняем
    // новые значения для ширины и высоты
    case WM_SIZE:
    {
      cxClient = LOWORD(lParam);
      cyClient = HIWORD(lParam);
      return 0;
    }

    // Рисование в окне
    case WM_PAINT:
    {
      HBRUSH hbrush, hbrushOldBrush;
      int nCapHeight, nFrameHeight;

      // Рисуем в области заголовка окна

      // Получаем контекст отображения для
      // всего окна
      hdc = GetWindowDC(hwnd);

      // Определяем высоту заголовка окна
      nCapHeight = GetSystemMetrics(SM_CYCAPTION);

      // Определяем толщину рамки окна
      nFrameHeight = GetSystemMetrics(SM_CYFRAME);

      // Создаем кисть зеленого цвета
      hbrush = CreateSolidBrush(RGB(0,0xff,0));

      // Выбираем кисть в контекст отображения, сохраняя
      // идентификатор старой кисти
      hbrushOldBrush = SelectBrush(hdc, hbrush);

      // Рисуем зеленый прямоугольник в левой  части
      // заголовка окна
      Rectangle(hdc,
        2*nCapHeight, nFrameHeight,
        4*nCapHeight, nCapHeight);

      // Создаем и выбираем зеленую кисть
      hbrush = CreateSolidBrush(RGB(0xff,0,0));
      SelectBrush(hdc, hbrush);

      // Вписываем в прямоугольник эллипс
      // красного цвета
      Ellipse(hdc,
        2*nCapHeight, nFrameHeight,
        4*nCapHeight, nCapHeight);

      // Выбираем старую кисть
      SelectBrush(hdc, hbrushOldBrush);

      // Освобождаем контекст отображения,
      // позволяющий рисовать во всем окне
      ReleaseDC(hwnd, hdc);

      // Рисуем во внутренней области окна

      // Получаем контекст отображения для
      // рисования во внутренней области окна 
      hdc = BeginPaint(hwnd, &ps);

      // Устанавливаем метрическую систему
      // координат с началом координат в
      // левом нижнем углу внутренней области
      // окна
      SetMapMode(hdc, MM_LOMETRIC);
      SetViewportOrg(hdc, 0, cyClient);

      // Устанавливаем режим отображения и
      // растровую операцию
      SetBkMode(hdc, nBkMode);
      SetROP2(hdc, nROP2);

      // В зависимости от содержимого переменной
      // nFigures вызываем одну из функций рисования
      if(nFigures == CM_LINES)
      {
        DrawLines(hdc);      // прямые линии
      }
      else if(nFigures == CM_ARC)
      {
        DrawArc(hdc);        // дуга окружности
      }
      else if(nFigures == CM_RECT)
      {
        DrawRectangles(hdc); // прямоугольники
      }
      else if(nFigures == CM_POLYGON)
      {
        DrawPolygon(hdc);    // многоугольники
      }
      else if(nFigures == CM_ELLIPSE)
      {
        DrawEllipse(hdc);    // эллипс
      }
      else if(nFigures == CM_CIRCLE)
      {
        DrawCircle(hdc);     // окружность
      }
      else if(nFigures == CM_PIE)
      {
        DrawPie(hdc);        // сектор эллипса
      }
      else if(nFigures == CM_CHORD)
      {
        DrawChord(hdc);      // сегмент эллипса
      }
      else if(nFigures == CM_POLYLINE)
      {
        DrawPolyline(hdc);   // ломаная линия
      }

      // Освобождаем контекст отображения
      EndPaint(hwnd, &ps);
      return 0;
    }

    // Обработка сообщений от меню
    case WM_COMMAND:
    {
      switch (wParam)
      {
        case CM_HELPABOUT:
        {
          MessageBox(hwnd,
            "Liner, v.1.0\n"
            "Drawing Graphics Demo\n"
            "(C) Frolov A.V., 1994",
            "About Liner", MB_OK | MB_ICONINFORMATION);
          return 0;
        }

        // Режим непрозрачного фона
        case CM_OPAQUE:
        {
          // Записываем код режима
          nBkMode = OPAQUE;

          // Перерисовываем окно приложения
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        // Режим прозрачного фона
        case CM_TRANSPARENT:
        {
          nBkMode = TRANSPARENT;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        // Выбор растровой операции
        case CM_R2_BLACK:
        {
          // Записываем код растровой операции
          nROP2 = R2_BLACK;

          // Перерисовываем окно приложения
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOTMERGEPEN:
        {
          nROP2 = R2_NOTMERGEPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MASKNOTPEN:
        {
          nROP2 = R2_MASKNOTPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOTCOPYPEN:
        {
          nROP2 = R2_NOTCOPYPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MASKPENNOT:
        {
          nROP2 = R2_MASKPENNOT;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOT:
        {
          nROP2 = R2_NOT;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_XORPEN:
        {
          nROP2 = R2_XORPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOTMASKPEN:
        {
          nROP2 = R2_NOTMASKPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MASKPEN:
        {
          nROP2 = R2_MASKPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOTXORPEN:
        {
          nROP2 = R2_NOTXORPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_NOP:
        {
          nROP2 = R2_NOP;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MERGENOTPEN:
        {
          nROP2 = R2_MERGENOTPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_COPYPEN:
        {
          nROP2 = R2_COPYPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MERGEPENNOT:
        {
          nROP2 = R2_MERGEPENNOT;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_MERGEPEN:
        {
          nROP2 = R2_MERGEPEN;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }
        case CM_R2_WHITE:
        {
          nROP2 = R2_WHITE;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        // Выбор из меню "Draw"
        case CM_LINES:
        {
          nFigures = CM_LINES;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_POLYLINE:
        {
          nFigures = CM_POLYLINE;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_ARC:
        {
          nFigures = CM_ARC;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_RECT:
        {
          nFigures = CM_RECT;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_POLYGON:
        {
          nFigures = CM_POLYGON;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_ELLIPSE:
        {
          nFigures = CM_ELLIPSE;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_CIRCLE:
        {
          nFigures = CM_CIRCLE;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_PIE:
        {
          nFigures = CM_PIE;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        case CM_CHORD:
        {
          nFigures = CM_CHORD;
          InvalidateRect(hwnd, NULL, TRUE);
          return 0;
        }

        // Завершаем работу приложения
        case CM_FILEEXIT:
        {
          DestroyWindow(hwnd);
          return 0;
        }

        default:
          return 0;
      }
    }

    case WM_DESTROY:
    {
      PostQuitMessage(0);
      return 0;
    }

    default:
      break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

Исходные тексты обильно снабжены комментариями, поэтому мы сделаем только самые необходимые пояснения.

При регистрации класса окна указываются стили CS_HREDRAW и CS_VREDRAW, в результате чего окно перерисовывается при изменении его высоты или ширины:

wc.style = CS_HREDRAW | CS_VREDRAW;

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

Обработчик сообщения WM_PAINT два раза получает контекст отображения. Вначале он получает контекст отображения, позволяющий рисовать во всем окне приложения:

hdc = GetWindowDC(hwnd);

Используя этот контекст отображения, обработчик сообщения WM_PAINT рисует в области заголовка окна эллипс, вписанный в прямоугольник.

После этого указанный контекст отображения освобождается:

ReleaseDC(hwnd, hdc);

Далее обработчик сообщения WM_PAINT получает общий контекст отображения, связанный с внутренней областью окна, вызывая функцию BeginPaint. В этом контексте отображения устанавливается метрический режим отображения, причем в качестве единицы длины используется одна десятая миллиметра. Начало осей координат сдвигается в левый нижний угол:

SetMapMode(hdc, MM_LOMETRIC);
SetViewportOrg(hdc, 0, cyClient);

Далее в соответствии с содержимым глобальной переменной nBkMode устанавливается режим фона:

SetBkMode(hdc, nBkMode);

Первоначально в этой переменной находится значение OPAQUE. Вы можете изменить содержимое переменной nBkMode с помощью меню "Background Mode" на TRANSPARENT.

Аналогично устанавливается растровая операция, используемая для рисования:

SetROP2(hdc, nROP2);

Далее обработчик сообщения WM_PAINT анализирует содержимое переменной nFigures, которое сразу после запуска приложения равно 0. Когда вы выбираете строку из меню "Draw", в эту переменную записывается идентификатор выбранной из меню строки. При обработке сообщения WM_PAINT в зависимости от содержимого переменной вызывается одна из нескольких функций, рисующих различные фигуры. Исходные тексты этих функций вынесены в отдельный файл (листинг 2.2).

Перед возвратом управления контекст отображения освобождается функцией EndPaint.

Обработчик сообщения WM_COMMAND предназначен для меню. После каждого изменения режима или кода растровой операции вызывается функция InvalidateRect, в результате чего в очередь сообщений записывается сообщение WM_PAINT:

case CM_TRANSPARENT:
{
  nBkMode = TRANSPARENT;
  InvalidateRect(hwnd, NULL, TRUE);
  return 0;
}

Функции, рисующие изображения, мы вынесли в отдельный файл (листинг 2.2).

Листинг 2.2. Файл liner/drawfn.cpp

// ----------------------------------------
// Функции для приложения LINER
// ----------------------------------------
#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <mem.h>
#include "liner.hpp"

// Прототипы функций
void DrawLines(HDC);
void DrawPolyline(HDC);
void DrawArc(HDC);
void DrawRectangles(HDC);
void DrawPolygon(HDC);
void DrawEllipse(HDC);
void DrawCircle(HDC);
void DrawPie(HDC);
void DrawChord(HDC);

// Размеры внутренней области окна
extern short cxClient, cyClient;

// -------------------------------------------
// DrawLines
// Рисование линий различной толщины и стиля
// -------------------------------------------
void DrawLines(HDC hdc)
{
  HPEN hpenW10, hpenW50;
  HPEN hpenDot, hpenDash, hpenDashDot, hpenOldPen;

  // Создаем несколько перьев: два пера толщиной
  // 1 мм и 5 мм, пунктирное, штриховое и
  // штрих-пунктирное
  hpenW10      = CreatePen(PS_SOLID, 10,  RGB(0,0,0));
  hpenW50      = CreatePen(PS_SOLID, 50,  RGB(0,0,0));
  hpenDot      = CreatePen(PS_DOT, 0,     RGB(0,0,0));
  hpenDash     = CreatePen(PS_DASH, 0,    RGB(0,0,0));
  hpenDashDot  = CreatePen(PS_DASHDOT, 0, RGB(0,0,0));

  // Рисуем тонкую линию пером, выбранным в контекст
  // отображения по умолчанию, и подписываем эту линию
  MoveToEx(hdc, 100, 100, NULL);
  LineTo(hdc, 600, 100);
  TextOut(hdc, 700, 100, "PS_SOLID, 1 pixel", 17);

  // Выбираем перо толщиной 1 мм
  hpenOldPen = SelectPen(hdc, hpenW10);
  
  // Рисуем линию выбранным пером
  MoveToEx(hdc, 100, 200, NULL);
  LineTo(hdc, 600, 200);
  TextOut(hdc, 700, 200, "PS_SOLID, 1 mm", 14);

  // Перебираем остальные перья

  SelectPen(hdc, hpenW50);

  MoveToEx(hdc, 100, 300, NULL);
  LineTo(hdc, 600, 300);
  TextOut(hdc, 700, 300, "PS_SOLID, 5 mm", 14);

  SelectPen(hdc, hpenDot);

  MoveToEx(hdc, 100, 400, NULL);
  LineTo(hdc, 600, 400);
  TextOut(hdc, 700, 400, "PS_DOT", 6);

  SelectPen(hdc, hpenDash);

  MoveToEx(hdc, 100, 500, NULL);
  LineTo(hdc, 600, 500);
  TextOut(hdc, 700, 500, "PS_DASH", 7);

  SelectPen(hdc, hpenDashDot);

  MoveToEx(hdc, 100, 600, NULL);
  LineTo(hdc, 600, 600);
  TextOut(hdc, 700, 600, "PS_DASHDOT", 10);

  // Выбираем старое перо
  SelectPen(hdc, hpenOldPen);

  // Удаляем созданные нами перья
  DeletePen(hpenW10);
  DeletePen(hpenW50);
  DeletePen(hpenDot);
  DeletePen(hpenDash);
  DeletePen(hpenDashDot);
}

// -------------------------------------------
// DrawPolyline
// Рисование ломаной линии
// -------------------------------------------
void DrawPolyline(HDC hdc)
{
  // Массив координат точек излома
  POINT ptPoints[] =
  {
    {10, 10},   {100, 310},
    {40, 300},  {300, 15},
    {135, 340}, {113, 125},
    {250, 137}, {300, 300}
  };

  // Рисуем ломаную линию
  Polyline(hdc, ptPoints,
    sizeof ptPoints / sizeof ptPoints[0]);
}

// -------------------------------------------
// DrawArc
// Рисование дуги эллипса
// -------------------------------------------
void DrawArc(HDC hdc)
{
  HPEN hpenW10, hpenOldPen;

  // Создаем перо толщиной 1 мм и выбираем его
  hpenW10  = CreatePen(PS_SOLID, 10, RGB(0,0,0));
  hpenOldPen = SelectPen(hdc, hpenW10);

  // Рисуем дугу
  Arc(hdc,
    100, 600, // верхний левый угол прямоугольника
    800, 100, // нижний правый угол прямоугольника
    650, 650, // начало
    750, 0);  // конец

  // Выбираем старое перо
  SelectPen(hdc, hpenOldPen);

  // Удаляем созданное перо 
  DeletePen(hpenW10);
}

// -------------------------------------------
// DrawRectangles
// Рисование прямоугольников
// -------------------------------------------
void DrawRectangles(HDC hdc)
{
  HPEN hpenW10, hpenOldPen;
  HBRUSH hbrush, hbrushOldBrush;
  POINT pt[2];
  RECT rc = {350, 500, 500, 400};

  // Рисуем прямоугольник вокруг внутренней области окна
  // Так как установлен метрический режим отображения,
  // а размеры окна, передаваемые вместе с сообщением
  // WM_SIZE, выражены в пикселах, выполняем
  // преобразование физических координат в логические
  pt[0].x = 0;
  pt[0].y = cyClient;
  pt[1].x = cxClient;
  pt[1].y = 0;
  DPtoLP(hdc, pt, 2);

  // Создаем перо толщиной 1 мм и выбираем его
  hpenW10  = CreatePen(PS_SOLID, 10, RGB(0,0,0));
  hpenOldPen = SelectPen(hdc, hpenW10);

  // Рисуем прямоугольник
  Rectangle(hdc, pt[0].x, pt[0].y,
    pt[1].x, pt[1].y);

  // Выбираем серую кисть
  hbrush = GetStockBrush(GRAY_BRUSH);
  hbrushOldBrush = SelectBrush(hdc, hbrush);

  // Рисуем прямоугольник, закрашенный серым цветом
  Rectangle(hdc, 100, 500, 300, 50);

  // Создаем и выбираем кисть для штриховки
  hbrush = CreateHatchBrush(HS_DIAGCROSS,
     RGB(0,0,0));
  SelectBrush(hdc, hbrush);

  // Рисуем заштрихованный прямоугольник
  Rectangle(hdc, 50, 300, 500, 100);

  // Выбираем старое перо и кисть
  SelectPen(hdc, hpenOldPen);
  SelectBrush(hdc, hbrushOldBrush);

  // Заштриховываем прямоугольную область кистью
  // hbrush, которая НЕ ВЫБРАНА в контекст
  FillRect(hdc, &rc, hbrush);

  // Рисуем прямоугольник со скругленными углами
  RoundRect(hdc, 550, 200, 800, 100, 50, 50);

  // Удаляем созданные нами перо и кисть
  DeletePen(hpenW10);
  DeleteBrush(hbrush);}

// -------------------------------------------
// DrawPolygon
// Рисование многоугольника
// -------------------------------------------
void DrawPolygon(HDC hdc)
{
  HBRUSH hbrush, hbrushOldBrush;
  int nOldPolyFillMode;

  // Координаты вершин первого многоугольника
  POINT ptPoints1[] =
  {
    {10, 10},   {100, 310}, {40, 300},
    {300, 15},  {135, 340}, {113, 125},
    {250, 137}, {300, 300}
  };

  // Координаты вершин второго многоугольника
  POINT ptPoints2[] =
  {
    {310, 10},  {400, 310}, {340, 300},
    {600, 15},  {435, 340}, {413, 125},
    {550, 137}, {600, 300}
  };

  // Выбираем встроенную серую кисть
  hbrush = GetStockBrush(GRAY_BRUSH);
  hbrushOldBrush = SelectBrush(hdc, hbrush);

  // Рисуем первый многоугольник в режиме
  // заполнения ALTERNATE, установленном
  // по умолчанию
  Polygon(hdc, ptPoints1,
    sizeof ptPoints1 / sizeof ptPoints1[0]);

  // Устанавливаем режим заполнения WINDING
  nOldPolyFillMode = SetPolyFillMode(hdc, WINDING);

  // Рисуем второй многоугольник
  Polygon(hdc, ptPoints2,
    sizeof ptPoints2 / sizeof ptPoints2[0]);

  // Восстанавливаем старый режим заполнения
  SetPolyFillMode(hdc, nOldPolyFillMode);
  SelectBrush(hdc, hbrushOldBrush);
}

// -------------------------------------------
// DrawEllipse
// Рисование эллипса
// -------------------------------------------
void DrawEllipse(HDC hdc)
{
  POINT pt[2];

  // Эллипс будет вписан во внутреннюю область
  // окна, поэтому определяем координаты углов
  // в текущей (метрической) системе координат,
  // выполняя преобразование
  pt[0].x = 0;
  pt[0].y = cyClient;
  pt[1].x = cxClient;
  pt[1].y = 0;
  DPtoLP(hdc, pt, 2);

  // Рисуем эллипс
  Ellipse(hdc, pt[0].x, pt[0].y,
    pt[1].x, pt[1].y);
}

// -------------------------------------------
// DrawCircle
// Рисование окружности
// -------------------------------------------
void DrawCircle(HDC hdc)
{
  // Рисуем эллипс, вписанный в квадрат
  Ellipse(hdc, 100, 600,
    600, 100);
}

// -------------------------------------------
// DrawPie
// Рисование сектора круга
// -------------------------------------------
void DrawPie(HDC hdc)
{
  HPEN hpenW10, hpenOldPen;

  // Создаем перо и выбираем его
  hpenW10  = CreatePen(PS_SOLID, 10, RGB(0,0,0));
  hpenOldPen = SelectPen(hdc, hpenW10);

  // Рисуем сектор круга
  Pie(hdc,
    100, 600, 800, 100, 650, 650, 750, 0);

  // Выбираем старое перо и удаляем созданное
  SelectPen(hdc, hpenOldPen);
  DeletePen(hpenW10);
}

// -------------------------------------------
// DrawChord
// Рисование сегмента круга
// -------------------------------------------
void DrawChord(HDC hdc)
{
  HPEN hpenW10, hpenOldPen;

  hpenW10  = CreatePen(PS_SOLID, 10, RGB(0,0,0));
  hpenOldPen = SelectPen(hdc, hpenW10);

  // Рисуем сегмент круга
  Chord(hdc,
    100, 600, 800, 100, 650, 650, 750, 0);

  SelectPen(hdc, hpenOldPen);
  DeletePen(hpenW10);
}

Символические имена констант, используемые для идентификации строк меню, описаны в файле liner.hpp (листинг 2.3).

Листинг 2.3. Файл liner/liner.hpp

#define CM_HELPABOUT       301
#define CM_OPAQUE          302
#define CM_TRANSPARENT     303
#define CM_FILEEXIT        304
#define CM_POLYGON         305
#define CM_PIE             306
#define CM_CHORD           307
#define CM_ELLIPSE         308
#define CM_RECT            309
#define CM_ARC             310
#define CM_LINES           311
#define CM_POLYLINE        312
#define CM_CIRCLE          313

#define CM_R2_BLACK        201
#define CM_R2_NOTMERGEPEN  202
#define CM_R2_MASKNOTPEN   203
#define CM_R2_NOTCOPYPEN   204
#define CM_R2_MASKPENNOT   205
#define CM_R2_NOT          206
#define CM_R2_XORPEN       207
#define CM_R2_NOTMASKPEN   208
#define CM_R2_MASKPEN      209
#define CM_R2_NOTXORPEN    210
#define CM_R2_NOP          211
#define CM_R2_MERGENOTPEN  212
#define CM_R2_COPYPEN      213
#define CM_R2_MERGEPENNOT  214
#define CM_R2_MERGEPEN     215
#define CM_R2_WHITE        216

Меню приложения определено в файле ресурсов liner.rc (листинг 2.4).

Листинг 2.4. Файл liner/liner.rc

#include "liner.hpp"

APP_MENU MENU 
BEGIN
  POPUP "&File"
    BEGIN
      MENUITEM "E&xit",          CM_FILEEXIT
    END

  POPUP "&Draw"
    BEGIN
      MENUITEM "&Lines",         CM_LINES
      MENUITEM "&Polyline",      CM_POLYLINE
      MENUITEM "&Arc",           CM_ARC
      MENUITEM "&Rectangles",    CM_RECT
      MENUITEM "P&opygon",       CM_POLYGON
      MENUITEM "&Ellipse",       CM_ELLIPSE
      MENUITEM "C&ircle",        CM_CIRCLE
      MENUITEM "&Chord",         CM_CHORD
      MENUITEM "&Pie",           CM_PIE
    END

  POPUP "&Background Mode"
    BEGIN
      MENUITEM "&Opaque",        CM_OPAQUE
      MENUITEM "&Transparent",   CM_TRANSPARENT
    END

  POPUP "&ROP"
    BEGIN
      MENUITEM "R2_BLACK",       CM_R2_BLACK
      MENUITEM "R2_NOTMERGEPEN", CM_R2_NOTMERGEPEN
      MENUITEM "R2_MASKNOTPEN",  CM_R2_MASKNOTPEN
      MENUITEM "R2_NOTCOPYPEN",  CM_R2_NOTCOPYPEN
      MENUITEM "R2_MASKPENNOT",  CM_R2_MASKPENNOT
      MENUITEM "R2_NOT",         CM_R2_NOT
      MENUITEM "R2_XORPEN",      CM_R2_XORPEN
      MENUITEM "R2_NOTMASKPEN",  CM_R2_NOTMASKPEN
      MENUITEM "R2_MASKPEN",     CM_R2_MASKPEN
      MENUITEM "R2_NOTXORPEN",   CM_R2_NOTXORPEN
      MENUITEM "R2_NOP",         CM_R2_NOP
      MENUITEM "R2_MERGENOTPEN", CM_R2_MERGENOTPEN
      MENUITEM "R2_COPYPEN",     CM_R2_COPYPEN
      MENUITEM "R2_MERGEPENNOT", CM_R2_MERGEPENNOT
      MENUITEM "R2_MERGEPEN",    CM_R2_MERGEPEN
      MENUITEM "R2_WHITE",       CM_R2_WHITE
    END

  POPUP "&Help"
    BEGIN
      MENUITEM "&About...",      CM_HELPABOUT
    END
END

Файл liner.def используется для определения модуля приложения LINER (листинг 2.5).

Листинг 2.5. Файл liner/liner.def

; =============================
; Файл определения модуля
; =============================
NAME        LINER
DESCRIPTION 'Приложение LINER, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

2.5. Области

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

Создание области

Приложение может создать область прямоугольной формы, область в виде многоугольника, область эллиптической формы. Можно комбинировать область из двух других, выполняя при этом над областями логические операции объединения, пересечения и т. д.

Прямоугольная область

Для создания прямоугольной области предназначены функции CreateRectRgn и CreateRectRgnIndirect :

HRGN WINAPI CreateRectRgn(
   int nLeftRect, int nTopRect,
   int nRightRect, int nBottomRect);
HRGN WINAPI CreateRectRgnIndirect(const RECT FAR* lprc);

Обе эти функции создают область прямоугольной формы, размеры которой заданы координатами верхнего левого и правого нижнего углов (для функции CreateRectRgn) или при помощи структуры типа RECT (для функции CreateRectRgnIndirect). Функции возвращают идентификатор созданной области.

Заметьте, что область создается вне всякой связи с контекстом отображения. Область - это объект, принадлежащий GDI, поэтому его следует удалить после использования. Лучше всего для этого воспользоваться макрокомандой DeleteRgn, определенной в файле windowsx.h следующим образом:

#define DeleteRgn(hrgn) DeleteObject((HGDIOBJ)(HRGN)(hrgn))

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

HRGN WINAPI CreateRoundRectRgn(
   int nLeftRect, int nTopRect,
   int nRightRect, int nBottomRect,
   int nWidth, int nHeight);

Первые четыре параметра аналогичны параметрам функции CreateRectRgn. Они определяют координаты углов прямоугольной области. Последние два параметра, nWidth и nHeight, предназначены, соответственно, для определения размеров воображаемого эллипса, формирующего скругленные углы.

Область в виде многоугольника

Можно создать область в виде произвольного многоугольника. Для этого следует воспользоваться функцией CreatePolygonRgn :

HRGN WINAPI CreatePolygonRgn(
   const POINT FAR* lppt, // адрес массива точек
   int cPoints,           // размер массива
   int fnPolyFillMode);   // режим заполнения

Функция CreatePolyPolygonRgn создает область, состоящую из нескольких многоугольников:

HRGN WINAPI CreatePolyPolygonRgn(
  const POINT FAR* lppt,  // адрес массива точек
  int FAR* lpnPolyCounts, // адрес массива количества точек
                          //    в многоугольниках
  int cPolygons);         // количество многоугольников

Назначение параметров описанных выше двух функций аналогично назначению параметров функций рисования многоугольников Polygon и PolyPolygon (за исключением того, что при создании области не требуется указывать идентификатор контекста отображения).

Область эллиптической формы

Область эллиптической формы (или, как частный случай, круглой формы) можно создать при помощи функции CreateEllipticRgn :

HRGN WINAPI CreateEllipticRgn(
   int nLeftRect, int nTopRect,
   int nRightRect, int nBottomRect);

Параметры этой функции описывают координаты воображаемого прямоугольника, в который вписана область эллиптической формы.

Функция CreateEllipticRgnIndirect также используется для создания области в форме эллипса:

HRGN WINAPI CreateEllipticRgnIndirect(const RECT FAR* lprc);

В отличие от функции CreateEllipticRgn координаты прямоугольника задаются с помощью структуры типа RECT, указатель на которую передается через параметр lprc.

Комбинирование областей

Функция CombineRegion позволяет вам изменить существующую область, скомбинировав ее из двух других:

int WINAPI CombineRgn(
  HRGN hrgnDest, // новая область
  HRGN hrgnSrc1, // первая исходная область
  HRGN hrgnSrc2, // вторая исходная область
  int fnCombineMode); // режим комбинирования

Перед вызовом функции вам надо определить все три области. Функция объединит области hrgnSrc1 и hrgnSrc2, изменив соответствующим образом область hrgnDest.

Способ комбинирования областей зависит от значения параметра fnCombineMode:

Значение параметра fnCombineMode Способ образования области hrgnDest
RGN_AND Пересечение областей hrgnSrc1 и hrgnSrc2
RGN_OR Объединение областей hrgnSrc1 и hrgnSrc2
RGN_XOR Объединение областей hrgnSrc1 и hrgnSrc2 с исключением перекрывающихся областей
RGN_DIFF Область hrgnSrc1, которая не входит в область hrgnSrc2
RGN_COPY Область hrgnSrc1

В зависимости от результата выполнения операции функция CombineRegion может вернуть одно из следующих значений:

Значение Описание
ERROR Ошибка
NULLREGION Новая область пустая
SIMPLEREGION Новая область не является самопересекающейся (т. е. граница созданной области не пересекает саму себя)
COMPLEXREGION Создана самопересекающаяся область

Для облегчения комбинирования областей в файле windowsx.h определены макрокоманды, предназначенные для копирования, пересечения, объединения и вычитания областей. Все они созданы на базе только что описанной функции CombineRegion :

#define CopyRgn (hrgnDst, hrgnSrc) \
   CombineRgn(hrgnDst, hrgnSrc, 0, RGN_COPY)

#define IntersectRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_AND)

#define SubtractRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_DIFF)

#define UnionRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_OR)

#define XorRgn (hrgnResult, hrgnA, hrgnB) \
   CombineRgn(hrgnResult, hrgnA, hrgnB, RGN_XOR)

Перерисовка области

Вы можете отметить область как требующую перерисовки, вызвав функцию InvalidateRgn. В результате этого приложению будет передано сообщение WM_PAINT.

Приведем прототип функции InvalidateRgn:

void WINAPI InvalidateRgn(HWND hwnd, 
  HRGN hrgn, BOOL fErase);

Через параметр hwnd следует передать идентификатор окна, содержащего обновленную область hrgn.

Параметр fErase определяет необходимость стирания фона окна перед перерисовкой. Если этот параметр имеет значение TRUE, фон стирается, если FALSE - нет.

Если ваше приложение обновило содержимое области, но не во время обработки сообщения WM_PAINT, оно может удалить область из списка областей, подлежащих перерисовке, вызвав функцию ValidateRgn :

void WINAPI ValidateRgn(HWND hwnd, HRGN hrgn);

Другие операции

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

void WINAPI SetRectRgn(
  HRGN hrgn,
  int nLeftRect, int nTopRect,
  int nRightRect, int nBottomRect);

Изменение области выполняется быстрее ее удаления с последующим повторный созданием новой.

Для перемещения области предназначена функция OffsetRgn :

int WINAPI OffsetRgn(HRGN hrgn, int nX, int nY);

Эта функция сдвигает область на nX логических единиц по горизонтали и на nY логических единиц по вертикали, возвращая такие же значения, что и функция CombineRegion.

Вы можете сравнить две области при помощи функции EqualRgn :

BOOL WINAPI EqualRgn(HRGN hrgn1, HRGN hrgn2);

Эта функция возвращает TRUE, если области совпадают, и FALSE - если нет.

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

int WINAPI GetRgnBox(HRGN hrgn, RECT FAR* lprc);

Эта функция возвращает такие же значения, что и функция CombineRegion

Иногда требуется определить, находится ли заданная точка или прямоугольная область внутри другой области. Это можно сделать при помощи функций PtInRegion и RectInRegion.

Функция PtInRegion возвращает TRUE, если точка (nX,nY) находится внутри области hrgn:

BOOL WINAPI PtInRegion(HRGN hrgn, int nX, int nY);

Аналогично для прямоугольной области:

BOOL WINAPI RectInRegion(HRGN hrgn, const RECT FAR* lprc);

Закрашивание области

Для закрашивания области кистью, выбранной в контекст отображения, предназначена функция PaintRgn :

BOOL WINAPI PaintRgn(HDC hdc, HRGN hrgn);

Функция FillRgn также закрашивает область, но, в отличие от функции PaintRgn, эта функция использует кисть, идентификатор которой передается ей в качестве параметра hbrush:

BOOL WINAPI FillRgn(HDC hdc, HRGN hrgn, HBRUSH hbrush);

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

BOOL WINAPI FrameRgn(HDC hdc, HRGN hrgn,
  HBRUSH hbrush, int nWidth, int nHeight);

Параметры nWidth и nHeight определяют, соответственно, ширину и высоту кисти в пикселах, используемой для рисования границы.

Функция InvertRgn инвертирует цвета в указанной области:

BOOL WINAPI InvertRgn(HDC hdc, HRGN hrgn);

Все эти функции возвращают TRUE при успешном завершении или FALSE при ошибке.

Графический редактор битовых изображений Paint Brush, который поставляется вместе с операционной системой Windows, умеет закрашивать внутренние области замкнутых фигур. Для закраски вам достаточно, находясь в соответствующем режиме редактора, указать мышью любую точку внутри фигуры.

Для реализации описанной выше операции в программном интерфейсе GDI предусмотрены функции FloodFill и ExtFloodFill.

Для функции FloodFill необходимо указать идентификатор контекста отображения hdc, координаты точки nX и nY, а также цвет контура clrref, ограничивающего область:

BOOL WINAPI FloodFill(HDC hdc,
  int nX, int nY, COLORREF clrref);

Функция возвращает TRUE при успешном завершении или FALSE при ошибке (ошибка возникает в том случае, когда указанная точка имеет цвет clrref или она лежит вне области ограничения данного контекста отображения).

Для закраски используется кисть, выбранная в контекст отображения.

Функция ExtFloodFill аналогична функции FloodFill:

BOOL WINAPI ExtFloodFill(HDC hdc,
  int nX, int nY, COLORREF clrref,
  UINT fuFillType);

Эта функция имеет дополнительный параметр fuFillType, определяющий способ закраски области. Параметр может принимать значения FLOODFILLBORDER или FLOODFILLSURFACE. В первом случае закрашивается внутренняя область фигуры, ограниченная контуром, имеющим цвет clrref (как и при использовании функции FloodFill). Во втором случае закрашивается вся область, имеющая цвет clrref.

Функция ExtFloodFill возвращает TRUE при успешном завершении или FALSE при ошибке. Если значение параметра fuFillType равно FLOODFILLBORDER, ошибка может возникнуть из-за тех же причин, что и при выполнении функции FloodFill. Если же значение параметра fuFillType равно FLOODFILLSURFACE, ошибка может возникнуть из-за того, что цвет точки (nX,nY) не равен clrref.

Область ограничения

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

int WINAPI SelectClipRgn(HDC hdc, HRGN hrgn);

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

2.6. Приложение REGIONS

Для демонстрации использования комбинированных областей мы подготовили приложение REGIONS. Это приложение создает три области: прямоугольную и две эллиптические разного размера. Прямоугольная область объединяется с первой эллиптической областью. Из полученного результата вырезается вторая эллиптическая область (меньших размеров).

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

Рис. 2.27. Вывод текста с использованием области ограничения

Исходный текст приложения приведен в листинге 2.6.

Листинг 2.6. Файл regions/regions.cpp

// ----------------------------------------
// Приложение REGIONS
// Демонстрация использования области ограничения
// ----------------------------------------

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <mem.h>

// Прототипы функций
BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);

// Имя класса окна
char const szClassName[]   = "RegionsClass";

// Заголовок окна
char const szWindowTitle[] = "Regions";

// Размеры внутренней области окна
short cxClient, cyClient;

// Размеры символов 
int cxChar, cyChar;

// Текст для вывода в окне приложения
char const szText[] =
 "В интерфейсе GDI есть средства, позволяющие приложениям"
 " создавать области достаточно сложной формы из"
 " прямоугольных, многоугольных и эллиптических областей."
 " Такие области можно закрашивать или использовать"
 " в качестве маски при выводе графического изображения. "
 "В последнем случае область называется областью"
 " ограничения. Она должна быть выбрана в контекст"
 " отображения.";

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance, 
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine, 
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  // Инициализируем приложение
  if(!InitApp(hInstance))
      return FALSE;

  // После успешной инициализации приложения создаем
  // главное окно приложения
  hwnd = CreateWindow(
    szClassName,         // имя класса окна
    szWindowTitle,       // заголовок окна
    WS_OVERLAPPEDWINDOW, // стиль окна
    CW_USEDEFAULT,       // задаем размеры и расположение
    CW_USEDEFAULT,       // окна, принятые по умолчанию
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    0, 0, hInstance, NULL);      
                       
  // Если создать окно не удалось, завершаем приложение
  if(!hwnd)
    return FALSE;

  // Рисуем главное окно
  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// Выполняет регистрацию класса окна
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна
  // Записываем во все поля структуры нулевые значения
  memset(&wc, 0, sizeof(wc));

  wc.lpszMenuName  = NULL;
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.lpszClassName = (LPSTR)szClassName;

  // Регистрация класса
  aWndClass = RegisterClass(&wc);

  return (aWndClass != 0);
}

// =====================================
// Функция WndProc
// =====================================

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static TEXTMETRIC tm;
  static HRGN hrgn1, hrgn2, hrgn3, hrgnTemp, hrgnClip;
  HBRUSH hbrush;

  switch (msg)
  {
    case WM_CREATE:
    {
      // Определяем метрику шрифта
      hdc = GetDC(hwnd);
      GetTextMetrics(hdc, &tm);
      ReleaseDC(hwnd, hdc);

      // Высота и средняя ширина букв
      cyChar = tm.tmHeight + tm.tmExternalLeading;
      cxChar = tm.tmAveCharWidth;

      // Область ограничения не задана
      hrgnClip = NULL;

      return 0;
    }

    // При изменении размеров окна сохраняем
    // новые значения для ширины и высоты,
    // а также определяем область ограничения
    case WM_SIZE:
    {
      cxClient = LOWORD(lParam);
      cyClient = HIWORD(lParam);

      // Если область ограничения была определена раньше,
      // удаляем ее
      if(hrgnClip)
        DeleteRgn(hrgnClip);

      // Формируем область ограничения
      hrgnClip = CreateEllipticRgn(0, 0, cxClient, cyClient);

      // Временная область ограничения
      hrgnTemp = CreateEllipticRgn(0, 0, cxClient, cyClient);

      // Первая эллиптическая область
      hrgn1 = CreateEllipticRgn(0, 0, cxClient, cyClient);

      // Вторая эллиптическая область
      hrgn2 = CreateEllipticRgn(cxClient/3, cyClient/3,
         2*(cxClient/3), 2*(cyClient/3));

      // Прямоугольная область
      hrgn3 = CreateRectRgn(cxClient/20, cyClient/20,
         19*(cxClient/20), 19*(cyClient/20));

      // Комбинируем области
      UnionRgn(hrgnTemp, hrgn1, hrgn3);
      SubtractRgn(hrgnClip, hrgnTemp, hrgn2);

      // Удаляем временные области
      DeleteRgn(hrgn1);
      DeleteRgn(hrgn2);
      DeleteRgn(hrgn3);
      DeleteRgn(hrgnTemp);
      return 0;
    }

    // Рисование в окне
    case WM_PAINT:
    {
      RECT rc;

      // Получаем контекст отображения для
      // рисования во внутренней области окна 
      hdc = BeginPaint(hwnd, &ps);

      // Выбираем встроенную кисть зеленого цвета
      hbrush = CreateSolidBrush(RGB(0, 0xff, 0));

      // Обводим границы области
      FrameRgn(hdc, hrgnClip, hbrush, 2, 5);

      // Выбираем область ограничения в контекст
      // отображения
      SelectClipRgn(hdc, hrgnClip);

      // Определяем координаты прямоугольной
      // области для вывода текста
      rc.left = cxChar;
      rc.top  = 0;
      rc.right  = cxClient - cxChar;
      rc.bottom = cyClient;

      // Вывод текста
      DrawText(hdc, szText, lstrlen(szText), &rc,
        DT_LEFT | DT_WORDBREAK);

      // Освобождаем контекст отображения
      EndPaint(hwnd, &ps);
      return 0;
    }

    case WM_DESTROY:
    {
      // удаляем область ограничения
      DeleteRgn(hrgnClip);

      PostQuitMessage(0);
      return 0;
    }

    default:
      break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

Во время создания окна обработчик сообщения WM_CREATE определяет метрику шрифта и записывает значение NULL в переменную hrgnClip. Эта переменная будет использоваться для хранения идентификатора области ограничения.

Область ограничения формируется каждый раз заново при изменении размеров окна. Обработчик сообщения WM_SIZE сохраняет ширину и высоту окна в переменных cxClient и cyClient.

Затем он проверяет содержимое переменной hrgnClip. Если была задана область ограничения, она удаляется, так как при изменении размеров окна нужно сформировать новую область ограничения.

После этого приложение создает области ограничения и комбинирует их в одну область hrgnClip, используя макрокоманды UnionRgn и SubtractRgn.

Далее все области, кроме hrgnClip, удаляются, так как они больше не нужны.

Приложение REGIONS рисует в окне во время обработки сообщения WM_PAINT.

Для большей наглядности обработчик этого сообщения обводит контуры области ограничения, вызывая функцию FrameRgn:

hbrush = CreateSolidBrush(RGB(0, 0xff, 0));
FrameRgn(hdc, hrgnClip, hbrush, 2, 5);

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

SelectClipRgn(hdc, hrgnClip);

Вывод текста выполняется при помощи функции DrawText.

Перед завершением своей работы (при обработке сообщения WM_DESTROY) приложение удаляет область hrgnClip, вызывая макрокоманду DeleteRgn:

DeleteRgn(hrgnClip);

Файл определения модуля для приложения REGIONS приведен в листинге 2.7.

Листинг 2.7. Файл regions/regions.def

; =============================
; Файл определения модуля
; =============================
NAME        REGIONS
DESCRIPTION 'Приложение REGIONS, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple

2.7. Сохранение и восстановление контекста отображения

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

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

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

int WINAPI SaveDC(HDC hdc);

Значение, возвращаемое этой функцией, необходимо использовать в качестве параметра nSavedDC для функции RestoreDC, восстанавливающей атрибуты контекста отображения:

BOOL WINAPI RestoreDC(HDC hdc, int nSavedDC);

Функция RestoreDC возвращает значение TRUE при успешном завершении или FALSE при ошибке.

В качестве значения параметра nSavedDC можно использовать -1. В этом случае будет восстановлен контекст, сохраненный при последнем вызове функции SaveDC.

2.8. Приложение DASHLINE

Приложение DASHLINE демонстрирует использование функции LineDDA для рисования пунктирных линий увеличенной толщины (рис. 2.28). Напомним, что вы не можете создать перо для рисования таких линий обычными средствами.

Рис. 2.28. Пунктирные линии увеличенной толщины

Основной файл исходного текста приложения приведен в листинге 2.8.

Листинг 2.8. Файл dashline/dashline.cpp

// ----------------------------------------
// Приложение DASHLINE
// Демонстрация использования функции LineDDA
// ----------------------------------------

#define STRICT
#include <windows.h>
#include <windowsx.h>
#include <mem.h>

// Прототипы функций
BOOL InitApp(HINSTANCE);
LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM);

void CALLBACK _export 
  LineProc(int xPos, int yPos, LPSTR lphdc);

// Имя класса окна
char const szClassName[]   = "DashLineClass";

// Заголовок окна
char const szWindowTitle[] = "Dash Line";

// Идентификатор копии приложения
HINSTANCE hInst;

// =====================================
// Функция WinMain
// =====================================
#pragma argsused

int PASCAL
WinMain(HINSTANCE hInstance, 
        HINSTANCE hPrevInstance,
        LPSTR     lpszCmdLine, 
        int       nCmdShow)
{
  MSG  msg;   // структура для работы с сообщениями
  HWND hwnd;  // идентификатор главного окна приложения

  // Инициализируем приложение
  if(!InitApp(hInstance))
      return FALSE;

  // Сохраняем идентификатор копии приложения
  hInst = hInstance;

  // Создаем главное окно приложения
  hwnd = CreateWindow(
    szClassName,         // имя класса окна
    szWindowTitle,       // заголовок окна
    WS_OVERLAPPEDWINDOW, // стиль окна
    CW_USEDEFAULT,       // задаем размеры и расположение
    CW_USEDEFAULT,       // окна, принятые по умолчанию
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    0, 0, hInstance, NULL);      
                       
  // Если создать окно не удалось, завершаем приложение
  if(!hwnd)
    return FALSE;

  // Рисуем главное окно
  ShowWindow(hwnd, nCmdShow);
  UpdateWindow(hwnd);

  // Запускаем цикл обработки сообщений
  while(GetMessage(&msg, 0, 0, 0))
  {
    DispatchMessage(&msg);
  }
  return msg.wParam;
}

// =====================================
// Функция InitApp
// Выполняет регистрацию класса окна
// =====================================

BOOL
InitApp(HINSTANCE hInstance)
{
  ATOM aWndClass; // атом для кода возврата
  WNDCLASS wc;    // структура для регистрации
                  // класса окна
  // Записываем во все поля структуры нулевые значения
  memset(&wc, 0, sizeof(wc));

  wc.lpszMenuName  = NULL;
  wc.style         = CS_HREDRAW | CS_VREDRAW;
  wc.lpfnWndProc   = (WNDPROC) WndProc;
  wc.cbClsExtra    = 0;
  wc.cbWndExtra    = 0;
  wc.hInstance     = hInstance;
  wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
  wc.lpszClassName = (LPSTR)szClassName;

  // Регистрация класса
  aWndClass = RegisterClass(&wc);

  return (aWndClass != 0);
}

// =====================================
// Функция WndProc
// =====================================

LRESULT CALLBACK _export
WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static FARPROC lpfnLineProc;
  HPEN hpen, hpenOldPen;

  switch (msg)
  {
    case WM_CREATE:
    {
      // Создаем переходник для функции LineProc 
      lpfnLineProc = 
         MakeProcInstance((FARPROC)LineProc, hInst);
      return 0;
    }

    // Рисование в окне
    case WM_PAINT:
    {
      RECT rc;

      // Получаем контекст отображения для
      // рисования во внутренней области окна 
      hdc = BeginPaint(hwnd, &ps);

      // Создаем перо толщиной 3 пиксела и выбираем
      // его в контекст отображения 
      hpen = CreatePen(PS_SOLID, 3, RGB(0, 0, 0));
      hpenOldPen = SelectPen(hdc, hpen);

      // Рисуем несколько штриховых линий,
      // используя выбранное перо

      LineDDA(50, 50, 300, 50,
        (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

      LineDDA(50, 50, 300, 100,
        (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

      LineDDA(50, 50, 50, 100,
        (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

      LineDDA(50, 100, 300, 100,
        (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

      LineDDA(300, 50, 300, 100,
        (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

      // Выбираем старое перо и удаляем созданное
      SelectPen(hdc, hpenOldPen);
      DeletePen(hpen);

      // Освобождаем контекст отображения
      EndPaint(hwnd, &ps);
      return 0;
    }

    case WM_DESTROY:
    {
      // Освобождаем переходник функции LineProc
      FreeProcInstance(lpfnLineProc);

      PostQuitMessage(0);
      return 0;
    }

    default:
      break;
  }
  return DefWindowProc(hwnd, msg, wParam, lParam);
}

// --------------------------------------------------
// Функция LineProc вызывается для каждой точки линии
// --------------------------------------------------
void CALLBACK _export
LineProc(int xPos, int yPos, LPSTR lphdc)
{
    // Счетчик точек
    static short cSpaces = 1;

    // Для каждой первой точки устанавливаем текущую
    // позицию пера
    if(cSpaces == 1)
    {
      MoveToEx(*(HDC FAR*) lphdc, xPos, yPos, NULL);
      cSpaces++;
    }

    // Для каждой десятой точки рисуем линию
    else if(cSpaces == 10)
    {
      LineTo(*(HDC FAR*) lphdc, xPos, yPos);
      cSpaces++;
    }

    // Для каждой двадцатой точки устанавливаем
    // текущую позицию пера и сбрасываем счетчик
    else if(cSpaces == 20)
    {
      MoveToEx(*(HDC FAR*) lphdc, xPos, yPos, NULL);
      cSpaces = 1;
    }
    else
        cSpaces++;
}

В процессе инициализации главного окна приложения при обработке сообщения WM_CREATE создается переходник для функции рисования, которая является функцией обратного вызова:

lpfnLineProc = 
   MakeProcInstance ((FARPROC)LineProc, hInst);

Рисование линий выполняется обработчиком сообщения WM_PAINT.

Этот обработчик получает контекст отображения и создает перо толщиной 3 пиксела, с помощью которого он будет рисовать пунктирную линию. Перо выбирается в контекст отображения:

hpen = CreatePen(PS_SOLID, 3, RGB(0, 0, 0));
hpenOldPen = SelectPen(hdc, hpen);

Далее приложение рисует несколько пунктирных линий, вызывая функцию LineDDA:

LineDDA(50, 50, 300, 50,
  (LINEDDAPROC)lpfnLineProc, (DWORD)(LPSTR)&hdc);

Идентификатор контекста отображения, необходимый для рисования линии, передается через последний параметр функции.

В функции рисования есть статический счетчик cSpaces, с помощью которого организуется цикл рисования штриховых линий. Первоначальное значение этого счетчика равно 1.

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

Файл определения модуля приложения DASHLINE приведен в листинге 2.9.

Листинг 2.9. Файл dashline/dashline.def

; =============================
; Файл определения модуля
; =============================
NAME        DASHLINE
DESCRIPTION 'Приложение DASHLINE, (C) 1994, Frolov A.V.'
EXETYPE     windows
STUB        'winstub.exe'
STACKSIZE   8120
HEAPSIZE    1024
CODE        preload moveable discardable
DATA        preload moveable multiple