Рисование в окне формы

Для того, чтобы рисовать в окне формы, нужно иметь связанный с этим окном объект Graphics. Этот объект создаётся функцией GdipCreateFromHWND, которая как параметр получает дескриптор окна HWnd. Но, к сожалению, не всегда значение свойства HWnd формы, доступное в окне Properties, соответствует HWnd отображаемого окна — дело в том, что окна формы устроены гораздо сложнее, чем кажется на первый взгляд. Образно говоря, на форме может существовать несколько наложенных друг на друга окон (клиентские окна типа WCLIENTWINDOW), и только на одном из них, расположенном «на самом верху», нужно рисовать. Поэтому может возникнуть ситуация, когда вы успешно создаёте объект Graphics, связанный с окном формы по его дескриптору (свойство HWnd), пытаетесь рисовать в этом окне, все функции отрабатывают нормально, но вы ничего не видите. А всё дело в том, что вы рисуете в окне, которое «перекрыто» другим клиентским окном.
В каких случаях возникают такие клиентские окна? Например, если форма объявлена как форма верхнего уровня (As Top Level), то для того, чтобы мы могли размещать на ней различные управляющие элементы, создаётся клиентское окно со своим HWnd, расположенное над основным окном и «загораживающее» его. Иная ситуация возникает, когда форма имеет полосы прокрутки (и неважно, видны они в данный момент или нет). Полосы прокрутки являются самостоятельными окнами (со своими HWnd), но на них, перекрывая, наложено ещё одно клиентское окно! И только тогда, когда ваша форма не является формой верхнего уровня и на ней отсутствуют полосы прокрутки, её свойство HWnd является именно тем дескриптором, который нужно передавать объекту Graphics. Во всех остальных случаях вы должны определить правильный HWnd окна.
Существует ещё одна проблема. Дело в том, что, используя GDIPlus, вы рисуете непосредственно в окне формы, поэтому всё нарисованное вами затирается, если над формой перемещается другое окно. Если вы пробовали рисовать на форме при помощи методов Draw, Circle или Box, то наверняка сталкивались с таким эффектом. Другое дело, если изображение выводится в управляющем элементе, например, в ImageBox. Все визуальные управляющие элементы устроены таким образом, что они при необходимости сами себя перерисовывают. При использовании GDIPlus вы должны перехватывать сообщение Windows, посылаемое окну в момент перемещения над ним другого окна, и повторно рисовать всё нарисованное ранее.
В этом разделе вы узнаете, как решить перечисленные выше проблемы.

Определение «правильного» HWnd

Для того, чтобы найти дескриптор «самого верхнего» окна в так называемой Z-последовательности клиентских окон, используется Windows API функция GetWindow. Вы должны вызвать эту функцию в том случае, если ваша форма имеет полосы прокрутки (свойство ScrollBars имеет значение, большее нуля) или если это форма верхнего уровня (свойство ShowWindow равно двум). В следующем фрагменте кода демонстрируется последовательность действий для определения «правильного» HWnd:

 
 #DEFINE GW_CHILD 5
 DECLARE Long GetWindow IN Win32API Long, Long
 hWnd = thisform.hWnd
 IF thisform.ShowWindow = 2          && Для As Top Level Form
    hWnd = GetWindow(hWnd, GW_CHILD)
 ENDIF
 IF thisform.ScrollBars > 0          && При наличии в окне полос прокрутки
    hWnd = GetWindow(hWnd, GW_CHILD)
 ENDIF

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

Перехват оконных сообщений

Возможно, вы знаете, что в Windows управление окнами происходит посредством обмена сообщениями. Например, когда вы щёлкаете мышью по области экрана, то в ответ на событие «нажата левая кнопка мыши» Windows определяет, по какому окну вы щёлкнули (то есть в область какого окна попадают координаты мыши) и посылает этому окну сообщение о том, что по нему «щёлкнули» левой кнопкой мыши. В классе, связанном с этим окном, существуют специальные методы, цель которых — обрабатывать полученные от Windows сообщения. Объект — экземпляр оконного класса имеет внутри себя скрытый от нас метод, получающий оконные сообщения и выполняющий команду, аналогичную DO CASE, в которой в соответствии с кодом сообщения выбирается тот или иной метод класса для его обработки. Примерно то же происходит и при работе с клавиатурой — при нажатии на клавишу Windows «смотрит», какое окно в настоящий момент является активным, и посылает ему сообщение, в теле которого находится код нажатой клавиши.
Сообщение Windows представляет собой структуру, объявление которой в C++ выглядит следующим образом:

 
 Typedef struct tag MSG {
    HWND hwnd;
    UINT msg;
    WPARAM wParam;
    LPARAM lParam;
    DWORD time;
    POINT pt;
    } MSG
 

Описание полей структуры MSG:

  • hWnd — это целое число, содержащее дескриптор окна, которому посылается сообщение
  • msg — идентификатор сообщения. Windows резервирует для собственных нужд 1024 сообщения с идентификаторами от 0x0000 до 0x03FF (обычно для обозначения значений идентификаторов сообщений используют шестнадцатеричный формат). Все сообщения, имеющие идентификаторы, начинающиеся с 0x0400, являются пользовательскими.
  • wParam — целое число, которое может содержать дополнительный параметр, используемый обработчиком этого сообщения (например, код нажатой клавиши)
  • lParam — дополнительный параметр, зависящий от типа сообщения; обычно это указатель на область памяти, в которой расположена дополнительная связанная с сообщением информация
  • time — значение системных часов во время генерации сообщения.
  • Pt — указатель на структуру POINT, содержащую координаты мыши.

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

Для того, чтобы решить проблему с затиранием рисунка в окне формы, нам требуется перехватывать всего два сообщения Windows: WM_PAINT и WM_ERASEBKGND.
Сообщение WM_PAINT (его идентификатор — 0x000F) посылается форме после того, как Windows нарисовала пустое окно; получив это сообщение, обработчик события посылает сообщения всем расположенным на форме визуальным управляющим элементам команду «нарисовать себя». Это сообщение посылается после создания окна или его восстановления из свёрнутого состояния.
Сообщение WM_ERASEBKGND (его идентификатор — 0x0014) посылается форме, если над ней перемещается другое окно. Получив это сообщение, обработчик события перерисовывает ту часть окна, которая вновь стала видимой.
Перехват оконных сообщений стал возможен в девятой версии Visual FoxPro в результате появления нового синтаксиса встроенной функции BINDEVENT:

 
 BINDEVENT(hWnd, nMessage, oEventHandler, cDelegate [, nFlags])
 

Функция перехватывает посланное окну сообщение, идентификатор которого указан в параметре nMessage, и вызывает метод cDelegate объекта oEventHandler для его обработки. Аргумент nFlag определяет тип связывания, его значение по умолчанию равно 0.
В момент получения сообщения оно будет направлено не оконному обработчику, а вашему пользовательскому методу, который содержится в вашем пользовательском объекте. Этому методу будет послано четыре параметра, содержащие значения полей hWnd, Msg, wParam и lParam структуры MSG.
В качестве объекта oEventHandler, обрабатывающего сообщения, может использоваться сама форма; соответственно, тогда метод cDelegate — это один из методов формы. Если в методе Init формы мы напишем код:

 
 #DEFINE WM_ERASEBKGND 0x0014
 BINDEVENT(this.hWnd, WM_ERASEBKGND, this, 'EventHandler', 0)
 

то в результате его выполнения посылаемое окну формы сообщение WM_ERASEBKGND будет перехватываться и для его обработки будет вызываться написанный вами метод EventHandler формы.

Если в методе EventHandler прописан код, перерисовывающий изображение — то проблема решена.
Но тут же возникает ещё одна проблема: если вы перехватите сообщение WM_PAINT и не ретранслируете его после обработки оконному обработчику сообщений, то на вашей форме вообще ничего не будет нарисовано, ни одного управляющего элемента! С сообщением WM_ERASEBKGND можно быть менее осторожным, хотя его так же лучше ретранслировать. Поэтому ваш метод-делегат должен иметь возможность ретрансляции оконных сообщений.
Как же правильно нужно перехватывать, обрабатывать и ретранслировать оконные сообщения? Для этого нам понадобится две Windows API функции:

  • GetWindowLong — функция возвращает адрес скрытого от нас обработчика сообщений окна
  • CallWindowProc — посылает поля структуры сообщения оконному обработчику сообщений.

В следующем фрагменте кода показан код, демонстрирующий перехват сообщений WM_PAINT и WM_ERASEBKGND и их обработка в самой форме:

 
 #DEFINE GWL_WNDPROC -4
 #DEFINE WM_PAINT 0x000F
 #DEFINE WM_ERASEBKGND 0x0014
 PUBLIC qnWndProc
 qnWndProc = GetWindowLong(thisform.hWnd, GWL_WNDPROC) 
 BINDEVENT(thisform.hWnd, WM_PAINT, thisform, 'EventHandler', 0)
 BINDEVENT(thisform.hWnd, WM_ERASEBKGND, thisform, 'EventHandler', 0)
 

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

 
 LPARAMETER hWnd, Msg, wParam, lParam
 = CallWindowProc(qnWndProc, hWnd, Msg, wParam, lParam)
 * Вызовы методов для рисования
 

Функция CallWindowProc вызывает оконный обработчик сообщений по его адресу, передаваемому ей в параметре qnWmdProc, и передаёт ему значения первых четырёх полей структуры MSG.

Создание связанного с окном объекта Graphics

Если вы полагаете, что мы обошли все подводные камни, то должен вас разочаровать. Объект Graphics при создании «запоминает» размеры клиентской области окна; если вы в последствии увеличите размеры этого окна, то появившиеся области будут недоступны объекту Graphics. Таким образом, объект Graphics должен создаваться заново всякий раз при изменении размеров окна.
Создаётся связанный с окном объект Graphics функцией GdipCreateFromHWND. Вот её объявление в Visual FoxPro:

 
 DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long hWnd, Long @ nativeGraphics
 

Первый передаваемый функции параметр содержит дескриптор окна HWnd, а во второй передаваемый по ссылке параметр записывается дескриптор созданного объекта Graphics. Если окно, в котором вы собираетесь рисовать, имеет неизменяемые размеры, то вы можете создать объект Graphics в методе Init формы. Иначе — объект должен многократно создаваться в методе Resize.

Форма для рисования

Создайте новую форму с именем в том же проекте, в котором мы разрабатывали классы GdipImages и GdipPrinter. Это должна быть обычная немодальная форма без полос прокрутки. Добавьте в форму новые свойства:

  • защищённое свойство WndProc, в котором будет храниться адрес обработчика сообщений окна

  • cвойство oGP — для хранения ссылки на объект — экземпляр класса GdipImages

  • свойство nativeGraphics — для хранения дескриптора объекта Graphics

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

Листинг 23.9. Метод Init формы

 
 this.oGP = CREATEOBJECT("GdipImages")
 this.WndProc = GetWindowLong(thisform.hWnd, -4)
 BINDEVENT(this.hWnd, 0x000F, this, 'EventHandler', 0)
 BINDEVENT(this.hWnd, 0x0014, this, 'EventHandler', 0)
 

Создавать объект Graphics будем в методе Resize формы. Вот его код:

Листинг 23.10. Метод Resize формы

 
 LOCAL lnGraphics
 lnGraphics = 0
 DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long, Long @
 = GdipCreateFromHWND(this.hwnd, @lnGraphics)
 this.nativeGraphics = lnGraphics
 

Для рисования на форме создадим метод ToDraw. Этот метод будет вызывать метод DrawImage класса GdipImages для рисования на форме изображения. Следовательно, перед созданием объекта формы должен быть создан объект — экземпляр класса GdipImages, и в память компьютера должно быть загружено изображение.
Код метода ToDraw приведён в листинге 23.11.

Листинг 23.11. Метод ToDraw формы

 
 IF this.nativeGraphics != 0
    this.oGP.DrawImage(this.nativeGraphics, 0, 0)
 ENDIF 
 

Добавьте в форму метод с именем EventHandler. Код этого метода показан в листинге 23.12.

Листинг 23.12. Метод EventHandler формы

 
 LPARAMETER hWnd, Msg, wParam, lParam
 DECLARE Long CallWindowProc IN WIN32API Long, Long, Long, Long, Long
 = CallWindowProc(this.WndProc, hWnd, Msg, wParam, lParam)
 this.ToDraw()
 

Теперь осталось решить, каким образом мы будем загружать в объект GdipImages изображение. Так как наш пример носит чисто демонстрационный характер, то поручим это методу DblClick формы. Таким образом, при двойном щелчке мышью по клиентской области формы должно появляться диалоговое окно Open, в котором вы сможете выбрать файл; затем этот файл загружается в память.
Код метода DblClick показан в листинге 23.13.

Листинг 23.13. Метод DblClick формы

 
 LOCAL lcFile
 lcFile = GETFILE('JPG|BMP|GIF|PNG")
 IF !EMPTY(lcFile)
    this.oGP.LoadFromFile(lcFile)
    this.Resize()
    this.ToDraw()
 ENDIF 

Сохраните форму, присвоив ей имя Demo1. Запустите форму на выполнение, убедившись, что по умолчанию установлена папка, в которой находится библиотека vfpgdiplus и файлы формы. Дважды щёлкните по форме мышью и выберите файл. Если вы нигде не ошиблись, то изображение появится в клиентской области формы.
Попробуйте перемещать над формой другие окна. Изображение не должно затираться.

Модифицируйте код метода ToDraw, указав значения ширины и высоты отображаемой области в вызове метода DrawImage объекта GdipImages:

 
 this.oGP.DrawImage(this.nativeGraphics, this.Width, this.Height)
 

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

 
 this.Cls
 

которая будет «стирать» всё ранее нарисованное на форме.

Класс для рисования в окне формы

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

Добавьте в библиотеку vfpgdiplus.vcx новый класс с именем GdipWindow, в качестве родительского класса выберите класс Custom. Этот класс предназначен для рисования в окне формы, используя методы класса GdipImages; поэтому объект — экземпляр этого класса должен создаваться после того, как создан объект — экземпляр класса GdipImages.
Добавьте в класс следующие свойства:

  • nativeGraphics —для хранения дескриптора объекта Graphics, связанного с окном
  • wndProc —для хранения адреса метода оконного класса, обрабатывающего оконные сообщения
  • oForm — для хранения ссылки на форму

Объявите эти свойства как защищённые и установите их начальные значения в нуль.

Метод Init

Этот метод получает ссылку на объект формы. Он определяет «правильный» HWnd окна и организует перехват оконных сообщений. Код метода приведён в листинге 23.14.

Листинг 23.14. Метод Init класса GdipWindow

 
 LPARAMETERS toForm
 LOCAL hWnd
 IF VARTYPE(toForm) = "O" .and. toForm.BaseClass = "Form"
    DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long, Long @
    DECLARE Long GetWindow IN WIN32API Long, Long
    DECLARE Long GetWindowLong IN WIN32API Long, Long
    DECLARE Long CallWindowProc IN WIN32API Long, Long, Long, Long, Long
    this.oForm = toForm
 * Определение "правильного" HWND
    hWnd = toForm.HWND
    IF toForm.ShowWindow = 2
       HWnd = GetWindow(hWnd, 5)
    ENDIF
    IF toForm.ScrollBars > 0
       hWnd = GetWindow(hWnd, 5)
    ENDIF
    this.WndProc = GetWindowLong(hWnd, -4)
    this.hwnd = hWnd
 * Перехват сообщений WM_PAINT и WM_ ERASEBKGND
    BINDEVENT(hWnd, 0x000F, this, 'EventHandler', 0) && WM_PAINT
    BINDEVENT(hWnd, 0x0014, this, 'EventHandler', 0) && WM_WM_ERASEBKGND 
 * Перехват события Resize
    BINDEVENT(toForm, "Resize", this, "ResizeEvent") && Событие Resize
 ELSE 
    RETURN .f.
 ENDIF 
 

Кроме сообщений Windows, в методе перехватывается событие Resize формы и вызывается метод ResizeEvent класса для его обработки.

Метод EventHandler

Метод получает перехваченное сообщение Windows и ретранслирует его обработчику оконных сообщений. Основное назначение метода — инициировать процесс перерисовки изображения на форме, для чего из него вызывается метод формы, выполняющий рисование. Вы должны создать этот метод в форме и написать в нём необходимый код.
Добавьте метод с именем EventHandler в класс. Код метода приведён в листинге 23.15.

Листинг 23.15. Метод EventHandler класса GdipWindow

 
 LPARAMETER hWnd, Msg, wParam, lParam
 = CallWindowProc(this.WndProc, hWnd, Msg, wParam, lParam)
 IF this.nativeGraphics = 0
    this.ResizeEvent()
 ENDIF 
 IF VARTYPE(this.oForm) = "O"
    this.oForm.ToDraw()
 ENDIF
 

Как видно из листинга, для корректной работы метода вы должны создать на форме метод с именем ToDraw, который будет отвечать за рисование. Конечно, это не лучшее решение при создании класса, но, с другой стороны, оно обеспечивает большую гибкость, потому что лучше создать новый метод формы, чем каждый раз модифицировать класс.
Обратите внимание: если объект Graphics на момент перехвата сообщения не существует (например, форма только что создана и ей послано сообщение WM_PAINT), то вызывается метод ResizeEvent для его создания.

Метод ResizeEvent

Этот метод вызывается после выполнения метода — обработчика события Resize формы. В нём создаётся связанный с формой объект Graphics. Добавьте метод в класс. Его код приведён в листинге 23.16.

Листинг 23.16. Метод ResizeEvent класса GdipWindow

 
 LOCAL lnGraphics
 lnGraphics = 0
 IF GdipCreateFromHWND(this.hwnd, @lnGraphics) = 0
    this.nativeGraphics = lnGraphics
 ENDIF
 

Последний метод, который нужно добавить в класс — это метод GetGraphics, который будет возвращать дескриптор связанного с формой объекта Graphics. Он содержит всего одну команду:

 
 RETURN this.nativeGraphics
 

Тестирование

Создайте в проекте форму верхнего уровня (As Top Level Form). В методе Init введите следующий код:

 
 this.oGPWindow = CREATEOBJECT("GdipWindow", this)
 IF VARTYPE(this.oGPWindow) != "O"
    RETURN .F.
 ENDIF 
 

В методе Unload очистите очередь сообщений командой

 
 CLEAR EVENTS
 

Добавьте в форму метод ToDraw и введите в него следующий код:

 
 qoGP.DrawImage(this.oGPWindow.GetGraphics(), 0, 0)
 

(предполагается, что перед вызовом формы будет создан объект — экземпляр класса GdipImages, ссылка на который будет храниться в глобальной переменной qoGP).

Сохраните форму, присвоив ей имя Demo2.

Для тестирования создайте новый программный файл с именем test18.prg и введите в него следующий код:

 
 PUBLIC qoGP
 SET DEFAULT TO (JUSTPATH(SYS(16)))
 SET CLASSLIB TO vfpgdiplus
 qoGP = CREATEOBJECT("GdipImages")
 IF qoGP.LoadFromFile(GETFILE("JPG"))
    DO FORM Demo2
    READ EVENTS
 ENDIF 
 RELEASE qoGP

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