Рисование в окне формы
Для того, чтобы рисовать в окне формы, нужно иметь
связанный с этим окном объект Graphics. Этот объект создаётся функцией
GdipCreateFromHWND, которая как параметр получает дескриптор окна HWnd. Но,
к сожалению, не всегда значение свойства HWnd формы, доступное в окне
Properties, соответствует HWnd отображаемого окна — дело в том, что окна
формы устроены гораздо сложнее, чем кажется на первый взгляд. Образно говоря,
на форме может существовать несколько наложенных друг на друга окон (клиентские
окна типа WCLIENTWINDOW), и только на одном из них, расположенном «на самом
верху», нужно рисовать. Поэтому может возникнуть ситуация, когда вы успешно
создаёте объект Graphics, связанный с окном формы по его дескриптору (свойство
HWnd), пытаетесь рисовать в этом окне, все функции отрабатывают нормально,
но вы ничего не видите. А всё дело в том, что вы рисуете в окне, которое «перекрыто»
другим клиентским окном. Определение «правильного» 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 «смотрит», какое окно в
настоящий момент является активным, и посылает ему сообщение, в теле
которого находится код нажатой клавиши. Typedef struct tag MSG { HWND hwnd; UINT msg; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG Описание полей структуры MSG:
В подавляющем числе достаточно анализировать первые четыре поля структуры. Для того, чтобы решить проблему с затиранием рисунка в
окне формы, нам требуется перехватывать всего два сообщения Windows:
WM_PAINT и WM_ERASEBKGND. BINDEVENT(hWnd, nMessage, oEventHandler, cDelegate [, nFlags]) Функция перехватывает посланное окну сообщение,
идентификатор которого указан в параметре nMessage, и вызывает метод
cDelegate объекта oEventHandler для его обработки. Аргумент nFlag определяет
тип связывания, его значение по умолчанию равно 0. #DEFINE WM_ERASEBKGND 0x0014 BINDEVENT(this.hWnd, WM_ERASEBKGND, this, 'EventHandler', 0) то в результате его выполнения посылаемое окну формы сообщение WM_ERASEBKGND будет перехватываться и для его обработки будет вызываться написанный вами метод EventHandler формы. Если в методе EventHandler прописан код, перерисовывающий
изображение — то проблема решена.
В следующем фрагменте кода показан код, демонстрирующий перехват сообщений 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 должен создаваться заново всякий раз при изменении размеров
окна. DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long hWnd, Long @ nativeGraphics Первый передаваемый функции параметр содержит дескриптор окна HWnd, а во второй передаваемый по ссылке параметр записывается дескриптор созданного объекта Graphics. Если окно, в котором вы собираетесь рисовать, имеет неизменяемые размеры, то вы можете создать объект Graphics в методе Init формы. Иначе — объект должен многократно создаваться в методе Resize. Форма для рисованияСоздайте новую форму с именем в том же проекте, в котором мы разрабатывали классы GdipImages и GdipPrinter. Это должна быть обычная немодальная форма без полос прокрутки. Добавьте в форму новые свойства:
Установите начальные значения этих свойств равными нулю. В метод Init введите код, показанный в листинге 23.9.
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 формы. Вот его код:
LOCAL lnGraphics lnGraphics = 0 DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long, Long @ = GdipCreateFromHWND(this.hwnd, @lnGraphics) this.nativeGraphics = lnGraphics Для рисования на форме создадим метод ToDraw. Этот
метод будет вызывать метод DrawImage класса GdipImages для
рисования на форме изображения. Следовательно, перед созданием объекта формы
должен быть создан объект — экземпляр класса GdipImages, и в память
компьютера должно быть загружено изображение.
IF this.nativeGraphics != 0 this.oGP.DrawImage(this.nativeGraphics, 0, 0) ENDIF Добавьте в форму метод с именем EventHandler. Код этого метода показан в листинге 23.12.
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, в котором вы сможете выбрать файл;
затем этот файл загружается в память.
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.
Объявите эти свойства как защищённые и установите их начальные значения в нуль. Метод InitЭтот метод получает ссылку на объект формы. Он определяет «правильный» HWnd окна и организует перехват оконных сообщений. Код метода приведён в листинге 23.14.
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 и
ретранслирует его обработчику оконных сообщений. Основное назначение метода
— инициировать процесс перерисовки изображения на форме, для чего из него
вызывается метод формы, выполняющий рисование. Вы должны создать этот метод
в форме и написать в нём необходимый код.
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, который будет отвечать за
рисование. Конечно, это не лучшее решение при создании класса, но, с другой
стороны, оно обеспечивает большую гибкость, потому что лучше создать новый
метод формы, чем каждый раз модифицировать класс. Метод ResizeEventЭтот метод вызывается после выполнения метода — обработчика события Resize формы. В нём создаётся связанный с формой объект Graphics. Добавьте метод в класс. Его код приведён в листинге 23.16.
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 выберите файл. После подтверждения выбора на экран будет выведена форма, и на ней нарисовано выбранное изображение. |