Занятие 3. Графический интерфейс GDI


Основные графические объекты Windows
Атрибуты контекста графических устройств

На этом занятии мы будем изучать графический интерфейс Windows (GDI) и пытаться рисовать, используя для этого вызовы функций Windows API.

Для того чтобы понять принципы создания графических объектов в операционной системе Windows, необходимо ознакомиться с контекстом графических устройств Graphics Device Context, или сокращенно DC. Наверно, лучшей аналогией для понимания DC будет блок настройки телевизора. Как там хранятся все параметры, управляющие яркостью, контрастностью, насыщенностью и т. д., так и в DC хранятся все данные, требуемые для рисования в окнах ОС Windows.

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

Основные графические объекты Windows

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

Кисть - это миниатюрное (несколько пикселов на несколько пикселов) растровое изображение. Основное назначение кистей в Windows - заполнять внутреннюю область замкнутых фигур и фоны окон. В системе имеются несколько готовых кистей. Но их можно создать и из растровых изображений по своему вкусу.

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

Особым типом графических объектов являются шрифты. Они представляют собой либо похожий на растровое изображение набор пикселов (в случае с матричными шрифтами), либо набор кривых, описывающих контур отображаемых букв, что имеет место в векторных шрифтах, частный случай которых - шрифты True Type.

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

Запустите Borland C++ Builder и создайте форму.

Caption = 'Увеличить часть'
   Enabled = False
   TabOrder = 5

Теперь очередь за исходным текстом самой программы. Сначала создадим заголовочный файл winprog.h со следующим содержимым:

//--------------------------------------
#ifndef winprogH
#define winprogH
//--------------------------------------
#include 
#include 
#include 
#include 
#include 
#include 
//--------------------------------------
class TForm1 : public TForm
{
__published: 
        TGroupBox *GroupBox1;
        TEdit *Edit1;
        TEdit *Edit2;
        TEdit *Edit3;
        TEdit *Edit4;
        TBevel *Bevel1;
        TPanel *Panel2;
        TButton *Button3;
        TButton *Button4;
        TButton *Button5;
        TButton *Button6;
        TButton *Button1;
        TButton *Button2;
        TRadioGroup *RadioGroup1;
        TGroupBox *GroupBox2;
        TEdit *Edit5;
        TEdit *Edit6;
        TEdit *Edit7;
        TLabel *Label1;
        TLabel *Label2;
        TLabel *Label3;
        TStatusBar *StatusBar1;
        void __fastcall FormDestroy(TObject *Sender);
        void __fastcall FormCreate(TObject *Sender);
        void __fastcall Button1Click(TObject *Sender);
        void __fastcall Button2Click(TObject *Sender);
        void __fastcall Button3Click(TObject *Sender);
        void __fastcall Button5Click(TObject *Sender);
        void __fastcall Button4Click(TObject *Sender);
        void __fastcall Button6Click(TObject *Sender);
private:
 void __fastcall SetupPen(void);
 void __fastcall RemovePen(void);
 void __fastcall ClearPicture(void);
public:
 virtual __fastcall TForm1(TComponent* Owner);
};
//--------------------------------------
extern TForm1 *Form1;
//--------------------------------------
#endif

Затем файл, описывающий саму форму и ссылки на все компоненты, размещенные на ней, а также обработчики событий от компонентов.

//--------------------------------------
#include 
#pragma hdrstop

#include "winprog.h"

//--------------------------------------
#pragma resource "*.dfm"

TForm1 *Form1;
HDC dc, mdc;
HBITMAP bmp = NULL, back = NULL;
HBRUSH brush = NULL;
HPEN pen;
unsigned short pix[64];

//--------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
}
//--------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
 int bits_per_pixel, planes;

 dc = CreateDC("DISPLAY", NULL, NULL, NULL);
 planes = GetDeviceCaps(dc, PLANES);
 bits_per_pixel = GetDeviceCaps(dc, BITSPIXEL);
 StatusBar1->SimpleText = "Ваш видеоадаптер имеет: цветовых плоскостей - " +
  IntToStr(planes)+ ", глубина цвета - " + IntToStr(bits_per_pixel) +
  " бит на пиксел";
 DeleteDC(dc);
 dc = GetDC(Handle);
 SetBkMode(dc, TRANSPARENT);
}
//--------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
 ReleaseDC(Handle, dc);
 DeleteDC(mdc);
}
//--------------------------------------
void __fastcall TForm1::SetupPen(void)
{
 unsigned short r_color, g_color, b_color;
 r_color = (unsigned short) StrToInt(Edit5->Text);
 g_color = (unsigned short) StrToInt(Edit6->Text);
 b_color = (unsigned short) StrToInt(Edit7->Text);
 r_color = (unsigned short)(r_color > 255 ? 255: r_color);
 g_color = (unsigned short)(g_color > 255 ? 255: g_color);
 b_color = (unsigned short)(b_color > 255 ? 255: b_color);
 // Создать перо и выделить его в контексте устройства
 pen = CreatePen( RadioGroup1->ItemIndex, 1, RGB(r_color, g_color, b_color));
 pen = SelectObject(dc, pen);
 // Создать кисть и выделить ее в контексте устройства
 brush = CreateSolidBrush( GetSysColor(COLOR_BTNFACE));
 brush = SelectObject(dc, brush);
}
//--------------------------------------
void __fastcall TForm1::RemovePen(void)
{
 pen = SelectObject(dc, pen);
 DeleteObject(pen);
 DeleteObject(brush);
}
//--------------------------------------
void __fastcall TForm1::ClearPicture(void)
{
 InvalidateRect(Handle, NULL,true);
 UpdateWindow(Handle);
}
//--------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 unsigned short temp[4], swap;
 char* endptr = NULL;
 RECT r;

 ClearPicture();

 pix[0] = pix[4] = temp[0] =
                (unsigned short)strtol((Edit1->Text).c_str(), &endptr, 16);
 pix[1] = pix[5] = temp[1] =
                (unsigned short)strtol((Edit2->Text).c_str(), &endptr, 16);
 pix[2] = pix[6] = temp[2] =
                (unsigned short)strtol((Edit3->Text).c_str(), &endptr, 16);
 pix[3] = pix[7] = temp[3] =
                (unsigned short)strtol((Edit4->Text).c_str(), &endptr, 16);

 for( int i = 1; i < 8; i++)
 {
  // Сместить данные вправо
  swap = temp[3];
  for( int j = 3; j > 0; j-) temp[j] = temp[j-1];
  temp[0] = swap;
  // Скопировать данные из массива tmp в массив pix
  memcpy(&pix[i*8],  &temp[0], sizeof(short)*4);
  memcpy(&pix[i*8+4], &temp[0], sizeof(short)*4);
 }

 bmp = CreateBitmap(8, 8, 1, 16, &pix[0]);
 brush = CreatePatternBrush(bmp);
 mdc = CreateCompatibleDC(dc);
 back = CreateCompatibleBitmap(dc, 100,100);
 SelectObject(mdc, back);

 r.left = 0;
 r.top = 0;
 r.right = 100;
 r.bottom = 100;

 FillRect(mdc, &r, brush);

 Button2->Enabled = true;

 BitBlt(
  dc,
  485, 85,
  100, 100,
  mdc,
  0, 0,
  SRCCOPY);

 DeleteObject(back);
 DeleteObject(bmp);
}

//--------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 ClearPicture();

 StretchBlt(
  dc,
  485, 85,
  100, 100,
  mdc,
  0, 0,
  10, 10,
  SRCCOPY);
}
//--------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
 SetupPen();
 ClearPicture();
 MoveToEx(dc,440,50,NULL);
 LineTo(dc, 640, 240);
 RemovePen();
}
//--------------------------------------
void __fastcall TForm1::Button4Click(TObject *Sender)
{
 POINT points[6]=
 {
  {450,50}, {620,50}, {450,150}, {620,150}, {450,230}, {620,230}
 };
 SetupPen();
 ClearPicture();

 Polyline(dc,&points[0], 6);

 RemovePen();

}
//--------------------------------------
void __fastcall TForm1::Button5Click(TObject *Sender)
{
 SetupPen();
 ClearPicture();

 Pie(dc, 400, 50, 650, 250, 570, 250, 550, 70);

 RemovePen();
}
//--------------------------------------
void __fastcall TForm1::Button6Click(TObject *Sender)
{
 SetupPen();
 ClearPicture();

 Ellipse(dc, 450, 80, 650, 220 );

 RemovePen();
}
//--------------------------------------

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

Вы также можете посмотреть фрагмент полученного изображения с увеличением, нажав кнопку "Увеличить часть".

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

Теперь, когда вы в курсе дела, рассмотрим исходный текст программы. Начнем с обработчика события, возникающего при создании окна формы. Здесь мы вызываем функцию API CreateDC("DISPLAY", NULL, NULL, NULL), возвращающую ссылку на контекст всего экрана дисплея. Далее производятся вызовы очень полезной системной функции GetDeviceCaps(), которая рассказывает нам о многих параметрах видеоадаптера. В нашем случае мы получаем количество цветовых плоскостей, на которые разбита видеопамять, и число битов видеопамяти, отдаваемых для хранения значения одного пиксела. Получив эти значения, мы показываем их строке состояния. Обратите внимание, что строки в C++ Builder могут быть объединены в одну с помощью перегруженного оператора "+". После того как мы узнали все, что нам нужно, контекст уничтожается и функцией GetDC() создается новый контекст, но уже не для всего дисплея, а лишь для клиентской части окна нашей программы. В качестве параметра функции GetDC() передается ссылка на окно (тип HWND), которую мы берем из переменной Handle класса формы. Последнее, что необходимо сделать, - задать функцией SetBkMode() прозрачный фон, чтобы через него была видна подложка формы. Если этого не сделать, то в просветах между штрихами линий, нарисованных несплошными перьями, будет проглядывать посторонний цвет. Не забудьте освободить занятые контексты устройств в момент закрытия формы (событие OnDestroy).

Для установки пера в нашей программе предусмотрен метод TForm1::SetupPen(). В его задачу входит создание и установка пера с заданными цветовыми и штриховыми характеристиками. Цвет берется из свойства Text трех строк редактирования: Edit5, Edit6 и Edit7. Далее текстовая строка преобразуется в целое число с помощью библиотечной функции StrToInt() и заносится в переменные цветовых составляющих. Поскольку максимальное значение логической цветовой составляющей в Windows равняется 255, то в SetupPen() производится проверка значений цветов на выход за этот предел. В случае слишком большого числа оно усекается до 255.

После получения цветов из свойства ItemIndex компонента RadioGroup1 считывается тип штриховки пера и передается в качестве первого параметра функции API CreatePen(), создающей перо для рисования. Вторым параметром этой функции является цвет пера, задаваемый константой типа COLORREF. Если вы посмотрите на исходный текст, то увидите, что цвет получается из трех цветовых составляющих: красной, зеленой и синей. Макроопределение RGB соединяет три цветовых компоненты в одно число типа COLORREF, которое и передается CreatePen(). Но мало просто создать перо. Оно должно быть явно привязано к контексту устройства, в котором производится рисование. Этим занимается функция API SelectObject(). В результате ее выполнения новое перо становится активным, а возвращается ссылка на перо, которое было активно до этого момента. Его мы сохраняем в переменной pen, с тем чтобы восстановить его по завершении процесса рисования. Это мы делаем той же самой функцией SelectObject(). Для того чтобы замкнутые фигуры заливались цветом фона, мы создаем кисть, вызвав функцию CreateSolidBrush(). Необходимый цвет передается ей как параметр. Само значение фонового цвета берется из системных структур функцией GetSysColor(). Она находит значение константы COLOR_BTNFACE. Именно этим цветом закрашиваются формы.

Дальнейшее освобождение ресурсов происходит внутри метода RemovePen(). Там же созданные нами перо и кисть уничтожаются вызовом DeleteObject(), чтобы освободить занятые ресурсы Windows.

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

Последняя вспомогательная функция - ClearPicture(). Она очищает область вывода графики, с тем чтобы новое изображение выводилось уже на чистую поверхность. Здесь лишь два вызова API. Первый, InvalidateRect(), помечает всю рабочую поверхность окна как требующую перерисовки. Второй вызов, UpdateWindow(), посылает оконной процедуре сообщение WM_PAINT, в ответ на которое окно должно перерисовать себя. В этот момент и происходит очистка.

Вот мы и дошли до описания обработчиков нажатия кнопок, которые задают логику работы нашего приложения. Начнем рассмотрение с обработчика нажатия кнопки "Создать картинку". После очистки экрана вызовом ClearPicture() данный обработчик считывает шестнадцатеричные числа из строк редактирования, преобразует их из строчного типа к типу long и усекает до размера числа типа unsigned short. Готовые значения заносятся в массив pix и tmp одновременно. Дальнейший алгоритм производит сдвиг данных вправо в массиве tmp и копирует их в массив pix. Так реализуется сдвиг пикселов на экране, образуя муаровый узор. Полученную цепочку байтов мы передаем последним параметром в стандартную функцию API для создания растровых изображений CreateBitmap(). Два первых параметра задают размер создаваемого изображения. Третий параметр - количество цветовых плоскостей в видеопамяти. Четвертый параметр указывает, что мы создаем картинку с цветовой глубиной 16 бит на пиксел.

Следующий этап представляет собой создание кисти на основе имеющегося растрового изображения. Для этого вызывается функция API CreatePatternBrush(). Но главное - процесс рисования - только начинается. Сначала необходимо создать в памяти еще один контекст устройства с параметрами как у оконного контекста. Ссылка на контекст устройства передается в качестве параметра для вызова API CreateCompatibleDC(). Затем мы вызываем функцию CreateCompatibleBitmap(). Она создает чистое растровое изображение без рисунка размером 100Ё100 пикселов с параметрами, задаваемыми контекстом устройства. Когда изображение готово, оно выделяется в контексте устройства, который мы с вами создали в памяти. Теперь контекст устройства содержит ссылку на наше пустое изображение. Мы раскрасим его кистью, созданной из введенных пользователем данных. Чтобы закрасить область, мы вызываем функцию FillRect(), которой передается структура, описывающая заполняемый квадрат.

Наконец можно показать готовую картинку на экране. Для этого в API Windows среди прочего существует важная функция BitBlt(). Она переносит изображение или его фрагмент из одного контекста устройства на другой. Нужно лишь правильно задать координаты для отрисовки. Самый последний параметр управляет логической операцией рисования. Манипулируя им, вы имеете возможность создавать разнообразные графические эффекты. Завершается рисование уничтожением использованных графических объектов.

Обработчик нажатия кнопки "Увеличить часть" намного проще. После очистки экрана он вызывает стандартную функцию API StretchBlt(). По сути своей эта функция ничем не отличается от BitBlt(). Разница состоит в том, что BitBlt() копирует изображения с контекста на контекст попиксельно, а StretchBlt() может копировать с масштабированием. В нашем примере мы говорим: "скопируй изображение размером 10Ё10 из контекста mdc в область с координатами (485, 85) контекста dc. При этом растяни его до размера 100Ё100". В результате вы увидите увеличенный фрагмент растровой картинки, которая была задана вами.

У кнопки "Линия" обработчик называется Button3Click(). Вначале он производит стандартные действия по установке заданного вами пера в контексте устройства и очистке области рисования. Следующим вызовом MoveToEx() текущее перо контекста перемещается без рисования в новую позицию. Обратите внимание на последний параметр функции. Если вы передадите через него структуру типа POINT, то MoveToEx() вернет вам координаты старой позиции пера. Это удобно, если вам необходимо рассчитать новые координаты относительно старых. Из новой позиции перо проводит линию до точки, которая задана нами в качестве параметров вызова API LineTo(). По завершении рисования текущее перо убирается.

Ломаная линия в обработчике кнопки "Ломаная" рисуется немного сложнее, чем обычная линия. Функции Polyline(), которая рисует ломаные линии, требуется массив точек с координатами линий, а последним ее параметром должно быть число таких точек в массиве.

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

Следует отметить, что сектор рисуется против часовой стрелки.

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

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


Атрибуты контекста графических устройств

Атрибут
Значение по умолчанию
Назначение
Цвет фона
Белый
Цвет заполнения фона
Прозрачность фона
OPAQUE
Определяет прозрачность фона. Если TRANSPARENT, то через фон видна подложка окна, если OPAQUE - то подложки окна не видно
Логический номер кисти
Белая кисть
Заполнение нарисованных фигур
Начало координат кисти
(0,0)
Смещение верхнего левого угла кисти от начала координат окна
Текущая позиция пера
(0,0)
Точка, в которой находится перо
Режим графического вывода
R2_COPYPEN
Логическая операция по смешиванию цветов пера и фона
Логический номер шрифта
System
Текущий шрифт
Тип геометрических координат
MM_TEXT
Задает единицы измерения координат и направление осей отсчета
Логический номер пера
Черное перо
Текущее перо для рисования
Интервал между символами
0
Дополнительный интервал между символами
Режим закрашивания многоугольников
Альтернативный
Задает правила закрашивания многоугольников, нарисованных функцией Polygon()
Режим растяжения
Черный по белому
Задает режим отображения при использовании функции StretchBlt()