ИЗО: рисуем в памяти и в не клиентской области

О рисовании в MS Windows

  • Автор: Still Zero
  • Уровень знаний: advanced
  • Подразделы: нет
  • Дата публикации: 03.11.2005

О окнах
Окна, как возможно вам известно, состоят из двух областей: «клиентской» и «не клиентской». К «не клиентской» области относится заголовок (caption), статус-бар, фрэйм (бордюр вокруг окна), вертикальный и горизонтальный скролл-бары. Все остальное, а это «внутренности» окна — клиентская часть.
Рисование в этих областях различается. В эти области приходят разные события. Так например, при рисовании клиентской части приходит событие WM_PAINT, а не клиентской — WM_NCPAINT.
Области имеют разные хэндлы. Хэндл клиентской части — MyWindow{Prop:ClientHandle}, хэндл не клиентской — MyWindow{Prop:Handle}.

Контекст устройства
Рисование происходит не на самом окне, а в его контексте (device context, DC). Применительно к окнам, я определяю контекст как некий слой, и на нем собственно и осуществляется рисование.

MSDN
Device context (контекст устройства) — это структура, которая определяет набор графических объектов и ассоциированных с ними атрибутов и графические режимы для вывода. Графические объекты включают перо (pen) для рисования линий, кисть (brush) для закраски, битмап (bitmap) для копирования или скроллирования части экрана, палитру для определения набора доступных цветов, регион для обрезания и других операций, и траекторию (path) для операций закраски и рисования.

Рисование в памяти
Контекст можно создать самому, т.е. создать еще один слой. Можно также, сделать копию контекста окна (CreateCompatibleDC). Т.е. получится текущий контекст окна и контекст-копия в памяти. На контексте в памяти также можно рисовать. А после того, как мы закончим отрисовку, можно скопировать (BitBlt) «рисунок» из памяти в текущий контекст окна.
Зачем это надо?

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

Пример реализации
Задача: нарисовать бордюр вокруг окна.
Как известно бордюр вокруг окна в терминах Клариона может быть: single или double. Тем самым достигается эффект «плоского» или «3d-окна». Нарисуем бордюр таким образом, чтобы окно всегда было «плоским». В обычной жизни это конечно же не нужно. Этот «эффект» 🙂 можно применить, например, к окну popup-меню.

Создайте новое приложение.
Объявите следующие прототипы API-функций:

GetDCEx(ULONG hWnd,ULONG hrgnClip,LONG flags),ULONG,PASCAL,NAME('GetDCEx')
ReleaseDC(ULONG hWnd,ULONG hDC),LONG,PASCAL,NAME('ReleaseDC')
CreateRectRgn(LONG nLeftRect,LONG nTopRect,LONG nRightRect,LONG nBottomRect),|
              ULONG,PASCAL,NAME('CreateRectRgn')
GetWindowRect(ULONG hWnd,LONG lpRect),BOOL,PASCAL,NAME('GetWindowRect')
GetClientRect(ULONG hWnd,LONG lpRect),BOOL,PASCAL,NAME('GetClientRect')
CreateSolidBrush(LONG crColor),ULONG,PASCAL,NAME('CreateSolidBrush')
FrameRect(ULONG aDC,LONG lpRect,ULONG aBrush),LONG,RAW,PASCAL,PROC,NAME('FrameRect')
FillRect(ULONG aDC,LONG lpRect,ULONG aBrush),LONG,RAW,PASCAL,PROC,NAME('FillRect')
GetDC(ULONG hWnd),ULONG,PASCAL,NAME('GetDC')
BitBlt(ULONG hdcDest,LONG nXDest,LONG nYDest,LONG nWidth,LONG nHeight,|
       ULONG hdcSrc,LONG nXSrc,LONG nYSrc,LONG dwRop),BOOL,PASCAL,NAME('BitBlt')
CreateCompatibleDC(ULONG hDC),ULONG,PASCAL,NAME('CreateCompatibleDC')
CreateCompatibleBitmap(ULONG hDC,LONG nWidth,LONG nHeight),|
                       ULONG,PASCAL,NAME('CreateCompatibleBitmap')
SelectObject(ULONG hDC,ULONG hGDIObj),ULONG,PASCAL,NAME('SelectObject'),PROC
GetSysColorBrush(LONG nIndex),ULONG,PASCAL,NAME('GetSysColorBrush')
DeleteObject(ULONG hObject),BOOL,RAW,PASCAL,NAME('DeleteObject')
DeleteDC(ULONG hdc),BOOL,PASCAL,NAME('DeleteDC')

Возможно не все из этих функций нам понадобятся. Также, при описании функций я не использую приведение типов к API-типам. Т.е. не пишу DWORD, INT и т.п., а использую родные Кларион-типы, т.е. LONG, ULONG, SHORT и т.п. Параметры-указатели я описываю как LONG, а при передаче таких параметров использую Кларион-функцию ADDRESS.

Создайте фрэйм и MDIChild-окно, которое будет вызываться из фрэйма. На дочернем окне создайте кнопку. Определение переменных производите в секции данных окна, а собственно код выполнения «повесьте» на кнопку.

По-поводу переменных, все хэндлы имеют тип ULONG, а API-структура RECT «по-нашему» GROUP. Описываем локальные переменные:

loc:SRCCOPY            EQUATE(00CC0020H)

loc:DCX_WINDOW         EQUATE(1)
loc:DCX_CACHE          EQUATE(2)
loc:DCX_CLIPSIBLINGS   EQUATE(010h)
loc:DCX_INTERSECTRGN   EQUATE(080h)

loc:DCX_USER           EQUATE(loc:DCX_WINDOW+|
                              loc:DCX_CACHE+|
                              loc:DCX_CLIPSIBLINGS+|
                              loc:DCX_INTERSECTRGN)

loc:DC                 ULONG     ! контекст окна
mem:DC                 ULONG     ! контекст в памяти
loc:hRgn               ULONG     ! хэндл региона
loc:Brush              ULONG     ! хэндл кисти
loc:hBitmap            ULONG     ! хэндл битмапа

loc:Rect               GROUP     ! структура "прямоугольника"
left                      LONG   ! соответствующие координаты
top                       LONG
right                     LONG
bottom                    LONG
                       END

hWnd                   ULONG     ! хэндл окна

На нажатие кнопки размещаем следующий код:

hWnd = 0{Prop:Handle}
if GetWindowRect(hWnd,address(loc:Rect))
   loc:hRgn = CreateRectRgn(loc:Rect.left,loc:Rect.top,loc:Rect.right,loc:Rect.bottom)
   if loc:hRgn
      loc:DC = GetDCEx(hWnd,loc:hRgn, loc:DCX_USER)
      if loc:DC ! узнали DC на котором будем рисовать
         mem:DC = CreateCompatibleDC(loc:DC) ! создали DС в памяти
         if mem:DC
            loc:hBitmap=CreateCompatibleBitmap(loc:DC,loc:Rect.right-loc:Rect.left,|
                                               loc:Rect.bottom-loc:Rect.top)
            if loc:hBitmap
               SelectObject(mem:DC,loc:hBitmap)
               Err#=BitBlt(mem:DC, 0, 0, loc:Rect.right-loc:Rect.left, |
               loc:Rect.bottom-loc:Rect.top, loc:DC, 0, 0, loc:SRCCOPY)

               loc:Brush = GetSysColorBrush(16)
               loc:Rect.right = loc:Rect.right-loc:Rect.left
               loc:Rect.bottom = loc:Rect.bottom - loc:Rect.top
               loc:Rect.left = 0
               loc:Rect.top = 0

               Err#= FrameRect(mem:DC,ADDRESS(loc:Rect),loc:Brush)
               Err# = DeleteObject(loc:Brush)

               loc:Rect.right -= 1
               loc:Rect.bottom - = 1
               loc:Rect.left += 1
               loc:Rect.top += 1

               loc:Brush = CreateSolidBrush(COLOR:WHITE)
               Err#= FrameRect(mem:DC,ADDRESS(loc:Rect),loc:Brush)
               Err# = DeleteObject(loc:Brush)

               Err# = BitBlt(loc:DC, 0, 0, loc:Rect.right-loc:Rect.left+2,|
                    loc:Rect.bottom-loc:Rect.top+2, mem:DC, 0, 0, loc:SRCCOPY)
               Err# = DeleteObject(loc:hBitmap)
            end
            Err# = DeleteDC(mem:DC)
         end
         Err# = ReleaseDC(hWnd,loc:DC)
      end
   end
end

Я думаю, что код достаточно прозрачен, поэтому остановлюсь только на некоторых моментах.

GetWindowRect возвращает координаты прямоугольника относительно декстопа (GetClientRect возвращает клиентские координаты, т.е. параметры RECT.left и RECT.top всегда будут равны нулю).

Я использую функцию GetDCEx, потому что эта функция более универсальна, чем GetDC. Последним параметром функции GetDCEx является флаг «как создавать контекст».

Последовательность функций GetDC, CreateCompatibleDC, CreateCompatibleBitmap, SelectObject, BitBlt служит для создания «копии» контекста в памяти.

Вместо функции GetSysColorBrush можно использовать функцию CreateSolidBrush.

Обратите внимание на то, что после создания (CreateXXX, GetXXX) всегда идет уничтожение (DeleteObject, ReleaseDC).

Если вы измените вызов функции FrameRect на FillRect, то увидите, что будет закрашена именно «не клиентская» область окна.

В последнем вызове функции BitBlt (копирование из памяти на экран) указаны «координаты прямоугольника + 2». Это связано с тем, что необходимо вернуть измененные координаты региона к своим первоначальным значениям.

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

Для сабклассинга я использую класс, написанный ранее. В нем есть небольшие отличия от ранее представленного класса. Так например типы UNSIGNED/SIGNED заменены на ULONG/LONG соответственно.

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

FlatDemo (379)
© Project Zero, 2005-2006. Все права защищены.