Контекст Устройства и WM_PAINT

Я уже говорил, что в Windows окно само отвечает за перерисовку себя. Для того чтобы окно осуществило перерисовку, оно должно получить сообщение WM_PAINT. Каким образом осуществляется перерисовка?

Обычно используют один из трех методов:

 - Рабочая область может быть восстановлена, если ее содержимое формируется с помощью каких-либо вычислений;
 - последовательность событий, формирующих рабочую область, может быть сохранена, а затем "проиграна" сколь угодно раз;
 - можно создавать виртуальное окно и направлять весь вывод в виртуальное окно, а при получении основным окном сообщения WM_PAINT копировать содержимое виртуального окна в основное.

Думаю, что читатель догадался, что в качестве виртуального окна используется контекст в памяти. Как его копировать, мы уже знаем. Но как рисовать на нем?

Установка текущей позиции

Для установки текущей позиции используется функция MoveToEx(). В файле заголовков wingdi.h эта функция описывается следующим образом:

  WINGDIAPI BOOL WINAPI MoveToEx(HDC, int, int, LPPOINT);
Первый аргумент - это контекст устройства, на котором мы будем рисовать, второй и третий - координаты точки, в которую мы устанавливаем текущую графическую позицию. Последний аргумент - указатель на структуру типа POINT, в которую функция запишет координаты старой текущей позиции. Структура типа POINT выглядит следующим образом:
  POINT struc
    ptX  dd ?
    ptY  dd ?
  POINT ends
Если при вызове функции указатель на структуру типа POINT равен NULL, то координаты старой текущей позиции не возвращаются.
Прорисовка одного пикселя

Прорисовать один пиксель в определенной позиции мы можем с помощью вызова функции SetPixel(), описанной в wingdi.h:

   WINGDIAPI COLORREF WINAPI SetPixel(HDC, int, int, COLORREF);
Первый три аргумента очевидны - контекст устройства вывода и координаты прорисовываемого пикселя. Но что такое COLORREF?

Здесь следует пояснить, что каждый пиксель на экране состоит из трех микроточек - красной, зеленой и синей. Каждая из этих микроточек может светится с интенсивностью от 0 (микроточка не светится) до 255 (максимальная яркость). Например, если светится только красная составляющая, то получаются цвета от темно-бордового (почти черного) до ярко красного. Комбинируя микроточки и их интенсивность, мы можем определить почти 17 миллионов цветов (будут ли они все поддерживаться на компьютере читателя, определяется видеоподсистемой компьютера). Обычно в таких случаях говорят об RGB-значениях цвета (red, green, blue - красный, зеленый, голубой).

Вернемся к COLORREF. Опять обратимся к заголовочному файлу, но на сей раз не к wingdi.h, а к windef.h:

  typedef DWORD   COLORREF;
Понятно, что COLORREF - это двойное слово. Оно кодируется следующим образом:
  0x00bbggrr
т.е. младший байт определяет интенсивность красного, второй - зеленого, третий - синего цвета.

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

Создание пера для рисования линий

Рисование графических примитивов производится с помощью перьев. В Windows'95 есть три предопределенных пера - черное (BLACK_PEN), белое (WHITE_PEN) и прозрачное (NULL_PEN). При создании окна по умолчанию ему присваивается черное перо. Хэндл каждого из них может быть получен с помощью функции GetStockObject(). Естественно, что программиста не может удовлетворитьь столь малое число перьев, поэтому для прорисовки линий можно воспользоваться пером, созданным в программе посредством вызова функции CreatePen(). Как всегда, обращаемся к файлам заголовков, в данном случае - к файлу wingdi.h:

  WINGDIAPI HPEN   WINAPI CreatePen(int, int, COLORREF);
Первый аргумент определяет стиль кисти. В wingdi.h эти стили описаны достаточно образно. Для того чтобы сохранить стиль этого описания (не путать со стилем кисти) я включил его третьим столбцом в таблице:

Возможные стили кисти

Стиль пера Значение Описание Эффект
PS_SOLID 0 ______ Сплошная линия
PS_DASH 1 ------ Пунктирная линия
PS_DOT 2 ...... Линия из точек
PS_DASHDOT 3 _._._._ Штрих-пунктирная линия (тире-точка)
PS_DASHDOTDOT 4 _.._.._ Штрих-пунктирная линия (тире-точка-точка)
PS_NULL 5   Прозрачное перо
PS_INSIDEFRAME 6   При рисовании замкнутой фигуры граница фигуры будет определятся по внешнему краю, а не по середине линии (если толщина пера более 1 пикселя)

Второй аргумент функции CreatePen() - толщина пера в логических единицах. Если этот аргумент равен 0, то толщина пера делается равной одному пикселю.

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

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

Нарисовать линию можно с помощью функции LineTo(). Она описана в файле wingdi.h:

  WINGDIAPI  BOOL WINAPI LineTo(HDC, int, int);
Первый аргумент - контекст устройства. Второй и третий аргументы - координаты точки, ДО КОТОРОЙ ОТ ТЕКУЩЕЙ ПОЗИЦИИ будет проведена линия. При успешной завершении функция возвращает TRUE.

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

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

Прямоугольник можно нарисовать, обратившись к функции Rectangle(). Ее описание содержится в файле wingdi.h:

  WINGDIAPI BOOL WINAPI Rectangle(HDC, int, int, int, int);
Аргумент первый понятен без объяснений - хэндл контекста устройства. Остальные аргументы - координаты верхнего левого и нижнего правого углов прямоугольника. TRUE возвращает при нормальном завершении операции. Прямоугольник автоматически заполняется цветом и способом, определяемым текущей кистью.
Рисование эллипса

Для рисования эллипса необходимо вызвать функцию Ellipse(), которая в wingdi.h описывается следующим образом:

  WINGDIAPI BOOL WINAPI Ellipse(HDC, int, int, int, int);
Первый аргумент - это, как всегда, контекст устройства. Для того чтобы понять, как определяется эллипс, предлагаю читателю обратиться к рисунку:

 

Определение аргумента функции Ellipse():
Ellipse

Как видно из рисунка, эллипс ограничен прямоугольником. Именно через координаты этого прямоугольника и определяется прорисовываемый эллипс. Второй и третий аргументы - координаты левого верхнего угла прямоугольника (на рисунке обозначены как UpX, UpY), четвертый и пятый аргументы - координаты нижнего правого угла (на рисунке обозначены как LowX, LowY).

Окружность является частным случаем эллипса. И в данном случае, если мы определим прямоугольник, у которого ширина равна высоте, т.е. квадрат, вместо эллипса получим окружность.

Как эллипс, так и окружность после прорисовки заполняются цветом и атрибутами текущей кисти.

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

Прямоугольник с закругленными краями рисуется с помощью функции RoundRect(). Из файла wingdi.h добываем ее описание.

  WINGDIAPI BOOL  WINAPI RoundRect(HDC, int, int, int, int, int, int);
Первые пять аргументов полностью идентичны аргументам функции Rectangle(). Последние два аргумента содержат ширину и высоту эллипса, определяющего дуги. После прорисовки прямоугольник закрашивается текущей кистью. В случае успешного завершения функция возвращает TRUE.
Рисование дуги и сектора эллипса

Возьмем из файла wingdi.h описание функции Arc(), которая используется для рисования дуги:

  WINGDIAPI BOOL  WINAPI Arc(HDC, int, int, int, int, int, int, int, int);
Первые пять аргументов полностью аналогичны аргументам функции Ellipse(). Непосредственно дуга определяется еще двумя точками. Первая - начало дуги - находится на пересечении эллипса, частью которого является дуга, и прямой, проходящей через центр прямоугольника и точку начала дуги. На рисунке начало дуги обозначено StartX, StartY. Вторая - конец дуги - определяется аналогично. Конец дуги обозначен EndX, EndY. Таким образом, для прорисовки дуги необходимо сначала определить точки StartX, StartY и EndX, EndY, после чего прорисовывать дугу. Дуга прорисовывается против часовой стрелки.

 

Ellipse

У функции Pie(), которая применяется для рисования сектора эллипса, набор аргументов и их обозначения абсолютно идентичны функции Arc().

Несколько слов о заполнении объектов

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

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

  WINGDIAPI HBRUSH  WINAPI CreateSolidBrush(COLORREF);
Единственный аргумент этой функции - цвет кисти (может, лучше сказать не кисти, а краски?).

Штриховая кисть создается с помощью функции CreateHatchBrush():

  WINGDIAPI HBRUSH  WINAPI CreateHatchBrush(int, COLORREF);
Первый аргумент этой функции - стиль штриховки. Возможные стили приведены в таблице.

Стили штриховки

Стиль штриховки Значение Описание Эффект
HS_HORIZONTAL 0 ---- Горизонтальная штриховка
HS_VERTICAL 1 ||||| Вертикальная штриховка
HS_FDIAGONAL 2 \\ Наклонная слева направо штриховка
HS_BDIAGONAL 3 ///// Наклонная справа налево штриховка
HS_CROSS 4 +++++ Штриховка крестиком
HS_DIAGCROSS 5 xxxxx Штриховка косым крестиком

Второй аргумент указывает цвет штриховки.

И наконец, с помощью функции CreatePatternBrush() мы можем создать кисть, которая при заполнении будет использовать bitmap. В wingdi.h она описана следующим образом:

  WINGDIAPI HBRUSH WINAPI CreatePatternBrush(HBITMAP);
Уже по типу аргумента видно, что единственным аргументом этой функции является хэндл bitmap'а.

Эти три функции при успешном завершении возвращают хэндл созданной кисти. В том случае, если произошла какая-то ошибка, возвращаемое значение равно NULL.

Демонстрационная программа

Текст программы:

.386
.model flat, stdcall
include win32.inc

extrn            BeginPaint:PROC
extrn            BitBlt:PROC
extrn            CreateCompatibleBitmap:PROC
extrn            CreateCompatibleDC:PROC
extrn            CreateHatchBrush:PROC
extrn            CreatePen:PROC
extrn            CreateWindowExA:PROC
extrn            DefWindowProcA:PROC
extrn            DeleteDC:PROC
extrn            DeleteObject:PROC
extrn            DispatchMessageA:PROC
extrn            Ellipse:PROC
extrn            EndPaint:PROC
extrn            ExitProcess:PROC
extrn            GetClientRect:PROC
extrn            GetMessageA:PROC
extrn            GetModuleHandleA:PROC
extrn            GetStockObject:PROC
extrn            LineTo:PROC
extrn            LoadCursorA:PROC
extrn            LoadIconA:PROC
extrn            MoveToEx:PROC
extrn            PatBlt:PROC
extrn            PostQuitMessage:PROC
extrn            Rectangle:PROC
extrn            RegisterClassA:PROC
extrn            SelectObject:PROC
extrn            SetPixel:PROC
extrn            ShowWindow:PROC
extrn            TranslateMessage:PROC
extrn            UpdateWindow:PROC

BITMAP struct
   bmType       dd ?
   bmWidth      dd ?
   bmHeight     dd ?
   bmWidthBytes dd ?
   bmPlanes     dw ?
   bmBitsPixel  dw ?
   bmBits       dd ?
BITMAP ends

SRCCOPY          = 00CC0020h
PATCOPY          = 00F00021h
TRUE             = 1
FALSE            = 0

.data

newhwnd          dd 0
msg              MSGSTRUCT   
wc               WNDCLASS    
Rect             RECT 

hDC              dd ?
hCompatibleDC    dd ?
PaintStruct      PAINTSTRUCT 
hCompatibleBitmap dd ?
hBitmap          dd ?
hOldBitmap       dd ?
hOldPen          dd ?
hOldBrush        dd ?

Pens             dd 2 dup(?)
Brushes          dd 2 dup(?)

hInstance        dd 0

szTitleName      db 'GraphDemo', 0
szClassName      db 'ASMCLASS32',0
x                dd ?
y                dd ?
p                dd ?

.code
;-----------------------------------------------------------------------------
start:
        call    GetModuleHandleA, 0
        mov     [hInstance], eax

; initialize the WndClass structure
        mov     [wc.clsStyle], CS_HREDRAW + CS_VREDRAW
        mov     [wc.clsLpfnWndProc], offset GraphDemoWndProc
        mov     [wc.clsCbClsExtra], 0
        mov     [wc.clsCbWndExtra], 0

        mov     eax, [hInstance]
        mov     [wc.clsHInstance], eax

        call    LoadIconA, 0, IDI_APPLICATION
        mov     [wc.clsHIcon], eax

        call    LoadCursorA, 0 ,IDC_ARROW
        mov     [wc.clsHCursor], eax

        call    GetStockObject, WHITE_BRUSH
        mov     [wc.clsHbrBackground], eax

        mov     [wc.clsLpszMenuName], 0
        mov     [wc.clsLpszClassName], offset szClassName

        call    RegisterClassA, offset wc

        call    CreateWindowExA, 0,offset szClassName,offset szTitleName, 
                WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 
                CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,0,0, 
                [hInstance], 0

        mov     [newhwnd], eax

        call    ShowWindow, [newhwnd], SW_SHOWNORMAL
        call    UpdateWindow, [newhwnd]

msg_loop:
        call    GetMessageA, offset msg, 0, 0, 0
        .if     eax != 0
                call    TranslateMessage, offset msg
                call    DispatchMessageA, offset msg
                jmp     msg_loop
        .endif
        call    ExitProcess, [msg.msWPARAM]


;-----------------------------------------------------------------------------
GraphDemoWndProc    proc uses ebx edi esi, hwnd:DWORD, wmsg:DWORD,
                    wparam:DWORD, lparam:DWORD

        cmp     [wmsg], WM_PAINT
        je      wmpaint
        cmp     [wmsg], WM_DESTROY
        je      wmdestroy

        call    DefWindowProcA, [hwnd],[wmsg],[wparam],[lparam]
        jmp     finish

wmpaint:
        ;---------------------------------------
        call    CreatePen, 1,0,0FF0000h
        mov     [dword ptr Pens], eax
        call    CreatePen, 3,0,000000h
        mov     [dword ptr Pens+4], eax

        call    CreateHatchBrush, 1,00000FFh
        mov     [dword ptr Brushes], eax
        call    CreateHatchBrush, 2,0FF0080h
        mov     [dword ptr Brushes+4], eax
        ;---------------------------------------
        call    GetClientRect, [hwnd], offset Rect
        call    BeginPaint, [hwnd], offset PaintStruct
        mov     [hDC], eax
        call    CreateCompatibleDC, [hDC]
        mov     [hCompatibleDC], eax
        call    GetClientRect, [hwnd], offset Rect

        call    CreateCompatibleBitmap, [hDC],[Rect.rcRight],[Rect.rcBottom]
        mov     [hCompatibleBitmap], eax

        call    SelectObject, [hCompatibleDC],[hCompatibleBitmap]
        mov     [hOldBitmap], eax

        call    PatBlt, [hCompatibleDC],0,0,[Rect.rcRight], 
                [Rect.rcBottom], PATCOPY
        ;---------------------------------------
        mov     [x], 0
        mov     [y], 0
        mov     [p], 0
  @@lp2:
        call    SetPixel, [hCompatibleDC],[x],[y],00FF0000h
        .if     eax == -1
                mov   [x], 0
                add   [y], 10h
        .endif
        add     [x], 10h
        .if     eax == -1
                cmp   [p], -1
                jz    @@lp3
        .endif
        mov     [p], eax
        jmp  @@lp2
  @@lp3:
        ;---------------------------------------
        call    SelectObject, [hCompatibleDC],[dword ptr Pens]
        mov     [hOldPen], eax
        call    MoveToEx, [hCompatibleDC],[Rect.rcLeft],[Rect.rcTop],0
        call    LineTo, [hCompatibleDC],[Rect.rcRight],[Rect.rcBottom]

        call    SelectObject, [hCompatibleDC],[dword ptr Pens+4]
        call    MoveToEx, [hCompatibleDC],[Rect.rcRight],[Rect.rcTop],0
        call    LineTo, [hCompatibleDC],[Rect.rcLeft],[Rect.rcBottom]
        call    SelectObject, [hCompatibleDC],[hOldPen]
        ;=======================================
        call    SelectObject, [hCompatibleDC],[dword ptr Brushes]
        mov     [hOldBrush], eax
        call    Rectangle, [hCompatibleDC],100,75,450,200

        call    SelectObject, [hCompatibleDC],[dword ptr Brushes+4]
        call    Ellipse, [hCompatibleDC],50,50,200,100
        call    SelectObject, [hCompatibleDC],[hOldBrush]
        ;=======================================
        call    BitBlt, [hDC],[PaintStruct.PSrcPaint.rcLeft], 
                [PaintStruct.PSrcPaint.rcTop], 
                [PaintStruct.PSrcPaint.rcRight], 
                [PaintStruct.PSrcPaint.rcBottom], 
                [hCompatibleDC], 
                [PaintStruct.PSrcPaint.rcLeft], 
                [PaintStruct.PSrcPaint.rcTop], 
                SRCCOPY

        call    DeleteObject, [dword ptr Pens]
        call    DeleteObject, [dword ptr Pens+4]
        call    DeleteObject, [dword ptr Brushes]
        call    DeleteObject, [dword ptr Brushes+4]

        call    SelectObject, [hCompatibleDC], [hOldBitmap]
        call    DeleteObject, [hCompatibleBitmap]
        call    DeleteDC, [hCompatibleDC]
        call    EndPaint, [hwnd],offset PaintStruct
        mov     eax, 0
        jmp     finish

wmdestroy:
        call    DeleteObject, [hBitmap]
        call    PostQuitMessage, 0
        mov     eax, 0
finish:
        ret
GraphDemoWndProc          endp

ends
end start