Вывод текста в окно, как способ визуализации разнообразного рода информации, является одной из основных задач в любом языке программирования. На пути изучения программирования на языке Ассемблера под Windows, очередным увесистым камнем преткновения является освоение принципов работы со строковыми данными (текстом) в рамках использования объектов графического интерфейса (окон). В последние десятилетия человечество заметно изменило механизмы взаимодействия пользователя с операционной системой в сторону использования графики, постепенно нивелируя роль текстовых режимов работы, объясняя это удобством использования. Текстовый режим в виде практически не потерявшей свой первоначальный вид консоли, ведущей своё начало еще от телетайпов и пишущих машинок, всё еще присутствует в составе перечисленных ОС. И даже в области применения современных графических интерфейсов, мы так и не отошли от основной формы выражения языка - письменности, потому как текст был и остается одним из превалирующих типов интерпретации информации. Если говорить о ранних этапах становления, то предшественницей системы Windows была MSDOS, в которой интерфейс командной строки функционировал в текстовом режиме. Помните, как в ней реализовывался вывод текста на ассемблере, который осуществлялся через функции прерывания 10h BIOS, либо попросту прямой записью данных в область видеопамяти (сегмент 0B800h).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
;------------------------------------------------------------------ ; процедура вывода символа напрямую в сегмент видеопамяти ; вход: DI = смещение, AL = ASCII выводимого символа, AH = атрибут ;------------------------------------------------------------------ vga_output: push es ; сохраним es push 0B800h pop es ; es = сегмент видеопамяти mov word [es:di], ax ; записываем в видеопамять символ. AL=символ, AH=атрибут. pop es ; восстановим es ret ; возврат |
С одной стороны всё было довольно просто и увлекательно, ведь экран можно было представить в виде телетайпа, а вывод отождествлялся с потоком символов, автоматически "заворачивающемся" (переносящемся) при окончании очередной строки. Видеопамять отображалась на физический дисплей видеоадаптера таким образом, что можно было не заботиться о переносе строк как посредством прямого вывода в видеопамять, так и через функции прерывания 10h. Текстовый режим представлял собой матрицу ячеек фиксированного размера (8x8, 8x14, 8x16, ...), что существенно упрощало создание объектов любой сложности, начиная от одиночных символов и заканчивая полнофункциональными оконными интерфейсами. С другой стороны, во времена низкоуровневого программирования вывода под MSDOS, существовали и проблемы:
- специфическая настройка под разные типы адаптера (CGA, VGA, SVGA): режимы, параметры вывода и прочее;
- низкоуровневые особенности отдельных видеокарт: порты, методы задания параметров отображения, наборы команд и прочее.
- бесконтрольный доступ к оборудованию: "в экран" мог беспрепятственно писать любой код, выполнявшийся в системе в данный момент времени.
Что же касается графики, то слишком много аспектов приходилось принимать во внимание при написании графических библиотек, учитывавших особенности распространенных видеоадаптеров. Да, мир тогда был совершенно иным и мы попросту к нему привыкли. А потом наступил Windows. Нельзя сказать, что он неожиданно постучался в двери бытия, скорее параллельно существовал, понемногу обращая на себя все более пристальное внимание сообщества, в том время как DOS пыталась удержать первенство при помощи различного рода расширителей DOS (DOS/4GW и подобные). И хотя Windows ранних версий (3.11/95/98/98se/Me) представляла собой всего-лишь графическую надстройку над DOS, она уже тогда была ориентирована на обеспечение псевдомногозадачности, в ней планировалось разделение общих аппаратных ресурсов станции между множеством "одновременно" выполняющихся приложений. К тому же, основное (в некоторых версиях излишнее) внимание в Windows было уделено использованию оконного графического интерфейса, функционал которого обеспечивался так называемым Интерфейсом графического устройства (GDI).
По описанным выше причинам, экран в Windows это уже среда, разделенная между разнообразными "одновременно" функционирующими графическими приложениями. Логично предположить, что в многозадачных операционных системах должны действовать более строгие правила, дабы исключить ситуации, когда приложения используют для вывода своей информации чужую область экрана (окно/часть окна стороннего приложения). Поэтому, вывод текста в окно графического интерфейса Windows далеко не такая тривиальная задача, как её аналог для текстового режима в MSDOS, и тут нам необходимо усвоить ряд правил:
- Текст в графическом пользовательском интерфейсе (GUI) Windows это не фиксированная по ширине и высоте матрица, начинающаяся с заранее определенных позиций (как это было в DOS). Текст может стартовать абсолютно в любом месте окна (имеет произвольные координаты X,Y), быть разного размера (высота/ширина), отображаться разнообразными шрифтами. Текст в графическом пользовательском интерфейсе - это объект графического пользовательского интерфейса, сгруппированный из пикселей, то есть картинка!. Каждый символ представлен набором точек, объединенных в единый рисунок.
- Текстовая консоль помнит весь произведенный в неё вывод. Другими словами, текст на экране в текстовом режиме MSDOS мог оставаться сколь угодно долго вплоть до того момента, пока он не перезаписывался или не производились иные действия (смена режима). В Windows приложение должно само "помнить" весь вывод, произведенный в окно и быть готовым перерисовать содержимое окна, когда система посылает окну соответствующее сообщение.
- Применительно к выводу в графическое окно считается что символы рисуются, а не печатаются.
- Интерфейс GDI обеспечивает уровень абстракции, скрывая от пользователя особенности реализации того или иного видеоадаптера (видеокарты). Таким образом принципы вывода унифицируются, больше нет необходимости заботиться о каких-то реализациях архитектуры, код и представление везде выглядят одинаково. Вы просто вызываете функцию Windows API, чтобы выполнить ту или иную задачу, а GDI внутри себя самостоятельно определяет, как заставить клиентскую видеокарту выполнить требуемую от неё работу.
Функции Win32 API, предназначающиеся для работы с текстом: TextOut, ExtTextOut, TabbedTextOut, DrawText, DrawTextEx, SetTextAlign, SetTextColor, PolyTextOut. Перед тем как приступить непосредственно к практической части изучения вывода текста в окно на ассемблере, необходимо сделать небольшое отступление и ознакомиться с некоторыми принципами организации графического вывода в операционных системах Windows.
Области (регионы)
Фактически объектами любого современного графического интерфейса являются окна, представляющие собой прямоугольные области произвольного размера. Пользовательское приложение, как и система, оперирует понятием окна как некой прямоугольной областью, ограничивающей вывод информации. Окно в Windows разделено на несколько областей:
- Рабочая область (working area) это окно целиком, то есть вся область, занимаемая окном на экране;
- Неклиентская область (non-client area) это часть окна, включающая в себя служебные элементы: рамка (бордюр), заголовок, меню, кнопки масштабирования, закрытия и восстановления и полосы прокрутки. Система по умолчанию берет на себя управление большинством аспектов неклиентской области.
- Клиентская область (client area) это оставшаяся часть окна, предназначенная для отображения собственной информации приложения (текст/графика). Например, текстовый редактор отображает документ в клиентской области главного окна. Пользовательское приложение отвечает за обработку клиентской области окна. Для того, чтобы контролировать клиентскую область окна (принимать ввод пользователя и отображать в ней информацию), приложению необходимо инициализировать оконную процедуру.
Клиентская область это фактически регион окна приложения, за обслуживание которого отвечает само приложение. За неклиентский регион (non-client area), бордюры, заголовок, ползунок, окна, отвечает ядро системы.
Дабы приложения не "портили" окна чужих приложений, по умолчанию Windows обеспечивает контроль за выводом посредством ограничения области отрисовки каждого окна только его собственной клиентской областью.
Надо учитывать, что размер этой клиентской области окна не постоянен, поскольку пользовательская активность является причиной постоянных изменений атрибутов окна (размер, видимость и прч.). Вот именно это обстоятельство и является ключевым в понимании природы графического интерфейса. Теперь давайте поговорим о действительных и недействительных областях.
(Не)действительные области
В какой-то момент времени изображение (содержимое) окон всех приложений переносится системой в память видеоадаптера (видеопамять) для непосредственного отображения на экране. Начиная с этого момента подразумевается, что системе известно содержимое части экрана, соответствующее всем окнам, контекст которых был только что перенесен. Ядро постоянно отслеживает объекты графического интерфейса, расположенные на экране: изменения размеров окон, их перемещение. Если никаких изменений в окнах приложений не происходит, то видеоадаптер обновляет содержимое экрана, периодически выбирая его из собственной видеопамяти. Если окна приложений начинают открываться/закрываться/перемещаться, то на экране появляются области, информация об изображении которых теряет актуальность, иными словами потеряна (неизвестна) для операционной системы. В этом случае система извещает окна, области (части, фрагменты) которых стали недействительными, о необходимости восстановить (перерисовать) собственное содержимое. Ведь на рабочем столе зачастую возникают ситуации, когда пользователь перетаскивает существующее стороннее окно либо поверх содержимого нашего окна, затем закрывает/вновь перемещает его, или просто сворачивает и разворачивает окно приложения. Поскольку часть окна (или окно целиком) перекрыта содержимым находящегося выше окна, оригинальная часть замещена (скрыта) частью содержимого перекрывающего окна, поэтому часть клиентская области перекрытого окна становится недействительной. Давайте перечислим виды областей:
- Недействительная (повреженная, требующая перерисовки) область (invalid region) - регион клиентской части окна приложения, содержимое которого потеряло актуальность и требует обновления (перерисовки).
- Действительная (не требующая перерисовки) область (valid region) - регион клиентской области окна приложения, графическая информация о котором известна операционной системе.
- Обновляемый регион (update region) - регион, описывающий все недействительные области определенного окна, требующие перерисовки.
- Недействительный (повреженный, требующий перерисовки) прямоугольник (invalid rectangle) - минимальный прямоугольник в клиентской части окна приложения, включающий в себя (покрывающий) поврежденную, требующую перерисовки, область.
В зависимости от метода получения информации об области обновления окна, Windows определяет недействительную область как наименьший прямоугольник, целиком включающий в себя область, требующую обновления, либо в виде точной информации об области обновления (многоугольник), либо в полях структуры, имеющей прототип PAINTSTRUCT
, возвращаемой функцией получения контекста.
В случае, когда система обнаруживает недействительную область в пределах клиентской части окна, она уведомляет приложение-владельца, посылая в его очередь сообщений сообщение WM_PAINT, которое сигнализирует о необходимости восстановить потерявшую актуальность область. Координаты недействительной области содержатся в дополнительных параметрах сообщения (структура pt типа POINT). В случае, когда в каком-либо окне оказывается сразу множество областей, подлежащих обновлению, в очередь сообщений приложения поступает только одно сообщение WM_PAINT, в котором определен многоугольник, включающая в себя все недействительные области.
Вот с этого момента для начинающего разработчика всё начинает усложняться :) Получается как-то нетривиально, ведь алгоритм программы должен быть спроектирован таким образом, чтобы в любой момент оконная процедура могла перерисовать содержимое собственного окна (или часть окна). В связи с этим у нас появляются следующие правила:
- Данные о всех состояниях перерисовываемого содержимого окна должны храниться в пользовательском приложении. Это существенно меняет подход к написанию приложений.
- Если мы имеем дело с окном (а не с полноэкранным режимом), то для упрощения алгоритма перерисовки содержимого (части) окна, весь вывод информации в окно настоятельно рекомендуется (если это возможно) производить одном месте кода - в обработчике сообщения WM_PAINT оконной процедуры.
- С целью увеличения скорости работы приложения следует обновлять только (недействительную) область окна, требующую обновления. Однако никто не запрещает обновлять окно целиком, просто надо учитывать, что в зависимости от "тяжести" содержимого окна, на полную отрисовку могут потребоваться существенные производительные затраты.
Если честно, перед начинающим программистом встают не такие уж и тривиальные задачи. Тут, как говорится, пока не пощупаешь все эти подходы на практике, принципа не поймешь. К примеру, если мы пишем эдакий простенький графический редактор, который предназначен для рисования мышью (абстрактная кривая) в основном окне, мы должны хранить все точки данной фигуры, потому как в случае перемещения окна нам потребуется отрисовать (восстановить) её всю целиком. Та же ситуация и с текстом, приложение должно хранить всю ту информацию, которая использовалась во всех элементах окна и на основании которой оно может в любое время окно обновить. При работе с текстовой информацией (выводе/вводе), код приложения должен сохранять данные в памяти, чтобы их можно было "по требованию" системы вывести на экран, тем самым обновив (перерисовав) актуальное содержимое окна. Таким образом, принимая во внимание всё вышеизложенное, можно сделать следующий вывод:
Контекст устройства [отображения]
Очевидно, что отрисовку можно производить как внутри кода обработки любого входящего сообщения (чаще всего WM_PAINT), так и вне процедуры обработки сообщений окна, например в процедуре обработки сообщений потока. Но, не все так просто как хотелось бы, и для этого, что бы начать "рисовать", требуется еще поизвести ряд подготовительных действий. В коде процедуры обработки сообщения, непосредственно перед началом рисования чего-либо в клиентской области "собственного" окна, приложению необходимо запросить у системы так называемое "разрешение" на подобные действия. Под разрешением подразумевается системная структура под названием контекст устройства отображения (контекст отображения). Советую перейти по приведенной ссылке и ознакомиться с материалом. После этого, пожалуй, продолжим.
Методы получения контекста устройства
Как уже упоминалось, для начала работы с контекстом устройства, требуется получить его описатель у операционной системы. Говоря простым языком, каждый раз, когда приложению надо что-то нарисовать, код при помощи специализированных функций должен запросить и получить дескриптор контекста устройства. А вот тут то начинается определенная конкретика. Дело в том, что методы запроса контекста устройства различаются в зависимости от условий и целей, при которых этот запрос выполняется:
Ситуация | Метод получения |
---|---|
Обновление актуального содержимого окна по требованию системы в ответ на входящее сообщение WM_PAINT | Рекомендовано использование контейнера из функций BeginPaint...EndPaint. Все рисование заключается внутри пары этих функций, между которыми вы можете вызывать любые GDI-функции для отрисовки в клиентской области. В этом случае контекст устройства возвращается функцией BeginPaint; |
|
В данном случае мы не можем использовать контейнер из функций BeginPaint...EndPaint, поскольку обрабатываем не сообщение WM_PAINT. В данной ситуации мы можем использовать:
|
В специфических (каких именно?) случаях. | Использование контейнера из функций CreateDC...DeleteDC для создания своего собственного контекста устройства; |
вот как-то примерно так:
1 2 3 4 5 6 |
invoke GetDC,[hwnd] ; Получить контекст устройства. mov [hdc],eax ; сохраним дескриптор ... ; Тут мы вызываем различные функции рисования. invoke ReleaseDC,[hwnd],[hdc] ; Освободить контекст устройства по окончании. |
Система координат
Опять же, если вспомнить систему MSDOS, то в ней начало координат располагалось в верхнем левом углу экрана. Традиционно ось X направлена слева направо, ось Y - сверху вниз.
Выше мы упоминали, что для начала рисования необходимо создать контекст устройства. Именно в момент создания контекста устройства, наряду с другими параметрами, устанавливается так называемый режим отображения, по умолчанию обозначаемый константой MM_TEXT (префикс MM_ (Mapping Mode, Режим Отображения)).
Для режима MM_TEXT характерно:
- В качестве начала координат принимается верхняя левая точка "устройства" вывода (обычно: окна). Дело в том, что в функциях API координаты (X,Y) - это привязочные (относительные, выровненные) координаты, относительно которых происходит отрисовка объекта, а по умолчанию "привязка" (выравнивание) выполнена к левому верхнему углу.
- Логическая система координат совпадает с физической: начало координат (0,0) соответствует физическому началу координат устройства (0,0).
- Каждая единица по оси X или Y соответствует одному пикселю экрана.
Дабы не было путаницы в соответствии логических координат физическим, стоит отметить, что конкретно в нашем случае устройством вывода является окно приложения, а не весь физический экран, поэтому начало координат (0, 0) находится в верхнем левом углу клиентской области окна приложения. Текст выводится в логической системе координат, связанной с окном приложения.
Метод 1: отрисовка в ответ на сообщение WM_PAINT
В данном разделе будет представлен метод, использующий вывод текста в окно в коде обработчика сообщения WM_PAINT. Данный метод называется консолидированным и его использование подразумевает применение функции BeginPaint для получения контекста устройства, и, соответственно, каркаса из функций BeginPaint..EndPaint в качестве контейнера для пользовательского кода отрисовки текста. Поэтому во всех примерах, относящихся к данному методу, мы будем применять единый алгоритм обработчика сообщения. Отличительные особенности данного метода кроются в специфике функции BeginPaint:
- не требуется явно вычислять недействительную область (поврежденный прямоугольник), поскольку функция возвращает координаты в выходной структуре pnt;
- рисование результативно только в недействительной области, информация в действительной области просто не выводится на экран; поврежденный прямоугольник образует область усечения, соответственно отрисовка в окне продуктивна только внутри данной области.
- автоматически делает действительной (валидной) ранее недействительную область, дабы система постоянно не посылала в очередь сообщений приложения сообщение WM_PAINT;
При этом, наше приложение может быть спроектировано таким образом, что в обработчиках других сообщений может содержаться логика, сообщающая системе о необходимости перерисовки (части) окна, то есть в них мы непосредственно не рисуем, но можем подготавливать некоторые параметры, передавать координаты перерисовываемой области окна, и затем отсылать за отрисовкой в обработчик сообщения WM_PAINT. Но, поскольку WM_PAINT является системным сообщением и не рекомендуется к самостоятельной отправке кодом приложения в свою же очередь сообщений, надо поступать хитрее - использовать для этого специальные функции. Например, для того, чтобы заставить систему сгенерировать сообщение WM_PAINT и одновременно задать область отрисовки, присутствуют функции InvalidateRect и InvalidateRgn. При использовании этих функций обновляемый регион (update region) становится непустым, и система, в ответ на это, отправляет в очередь сообщений приложения сообщение WM_PAINT, обработка которого в нашем коде приведет к требуемому результату. В некоторых случаях для достижения аналогичного результата может применяться связка функций InvalidateRect / InvalidateRgn + UpdateWindow, в некоторых функция RedrawWindow.
Пример 1: Вывод текста в окно с использованием функции DrawText
В этом примере мы будем производить вывод текста в наше окно при помощью функции DrawText в процедуре обработки сообщения WM_PAINT. В нашем приложении я решил выводить многострочный текст, поскольку слишком простые иллюстрации из категории Hello, World! несут в себе довольно мало практического смысла. Как мы помним, при рисовании в ответ на сообщение WM_PAINT, мы должны использовать следующий алгоритм:
- Получить указатель (дескриптор) на контекст устройства при помощи функции BeginPaint;
- Произвести рисование в клиентской области при помощи функции DrawText;
- Освободить указатель (дескриптор) на контекст устройства при помощи функции EndPaint;
За основу для нашего примера был взят шаблон оконного приложения и выполнена его доработка. В том же материале была описана вся структура шаблона, поэтому здесь мы перейдем сразу к рассмотрению нового кода, выполняющего, непосредственно, вывод текста в окно. Давайте для начала приведем полный исходный код получившегося приложения, а затем начнем анализировать (изменения в исходном шаблоне выделены цветом):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
format PE GUI 4.0 ; Формат PE. Версия GUI 4.0. entry start ; Точка входа include '%include%\win32a.inc' ; Подключаем стандартный инклюд. Зачем? _style equ WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU ; ручное задание стиля. ЭКВИВАЛЕНТЫ должны задаваться ДО основного кода _bk_color equ (255 or (255 shl 8)) or (255 shl 16) ; цвет фона (три компоненты RGB) _text_color equ 0 ; цвет текста = черный ;=== сегмент кода ============================================================ section '.text' code readable executable start: invoke GetModuleHandle,0 ; Получим дескриптор приложения. Параметр = 0 - значит вернуть дескриптор вызвавшего функцию модуля. mov [wc.hInstance],eax ; Сохраним дескриптор в структуру wc mov [wc.style], CS_HREDRAW or CS_VREDRAW ; Стили перерисовки окна целиком invoke LoadIcon,0,IDI_ASTERISK ; Загружаем иконку IDI_ASTERISK из ресурсов исполняемого файла mov [wc.hIcon],eax ; Сохраним в структуре окна invoke LoadCursor,0,IDC_ARROW ; Загружаем курсор IDC_ARROW из ресурсов исполняемого файла mov [wc.hCursor],eax ; Сохраним дескриптор курсора в структуре окна mov [wc.lpfnWndProc],WindowProc ; Зададим ссылку на нашу процедуру обработки окна mov [wc.lpszClassName],_class ; Зададим имя класса окна mov [wc.hbrBackground],COLOR_WINDOW+1 ; Зададим цвет кисти invoke RegisterClass,wc ; Зарегистрируем наш класс окна test eax,eax ; Проверим eax на ошибку (0). jz error ; Если 0 - то ошибка. Выводим и выходим. invoke CreateWindowEx,0,_class,_title,_style,128,128,512,512,NULL,NULL,[wc.hInstance],NULL ; Создадим экземпляр окна на основе зарегистрированного класса. в eax возвращает дескриптор окна. test eax,eax ; Проверим eax на ошибку (0). jz error ; Если 0 - то ошибка. Выводим и выходим. mov [wHMain],eax ; сохраним дескриптор окна ;--- цикл обработки сообщений ------------------------------------------------ msg_loop: invoke GetMessage,msg,NULL,0,0 ; Получаем сообщение из очереди сообщений приложения, помещает его в структуру msg. Результат выполнения в eax. or eax,eax ; Сравнивает eax с 0 jz end_loop ; Если 0 то пришло сообщение WM_QUIT - выходим из цикла ожидания сообщений, если не 0 - продолжаем обрабатывать очередь msg_loop_2: invoke TranslateMessage,msg ; Дополнительная функция обработки сообщения. Конвертирует некоторые сообщения и отправляет их обратно в очередь. Служит для обработки клавиатуры. invoke DispatchMessage,msg ; Пересылает сообщения соответствующим процедурам обработки сообщений (WindowProc ...). jmp short msg_loop ; В начало цикла error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK ; Вывод сообщения об ошибке end_loop: invoke ExitProcess,[msg.wParam] ; Выход из программы. Код выхода сохраняется в msg.wParam. Его надо передать системе ;--- процедура обработки сообщений окна (функция окна, оконная процедура) ---- proc WindowProc hWnd,wMsg,wParam,lParam push ebx esi edi ; сохраним все регистры cmp [wMsg],WM_DESTROY ; Проверим на WM_DESTROY je .wmdestroy ; на обработчик wmdestroy cmp [wMsg],WM_CREATE ; Проверим на WM_CREATE je .wmcreate ; на обработчик wmcreate cmp [wMsg],WM_PAINT ; Проверим на WM_PAINT je .wmpaint ; на обработчик wmpaint .defwndproc: invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam] ; Дефолтная функция. Обрабатывает сообщения, которыми наша программа не пользуется. jmp .finish .wmcreate: xor eax,eax jmp .finish .wmpaint: invoke BeginPaint,[hWnd],pnt mov [pHMain],eax ; Сохранить контекст (дескриптор) устройства invoke SetBkColor,[pHMain],_bk_color ; Зададим цвет фона invoke SetTextColor,[pHMain],_text_color ; Зададим цвет текста invoke GetClientRect,[hWnd],rect ; Поучить область вывода invoke DrawText,[pHMain],_text,-1,rect,DT_WORDBREAK ; Вывести текст в окно invoke EndPaint,[hWnd],pnt ; Освободить контекст устройства xor eax,eax jmp .finish .wmdestroy: ; Обработчик сообщения WM_DESTROY. Обязателен. invoke PostQuitMessage,0 ; Посылает сообщение WM_QUIT в очередь сообщений, что вынуждает GetMessage вернуть 0. Посылается для выхода из программы. Посылается только основным окном. xor eax,eax ; Если наша процедура окна обрабатывает какое-либо сообщение, то она должна вернуть 0. eax=0. Иначе программа поведет себя непредсказуемо. .finish: pop edi esi ebx ; восстановим все регистры ret endp ;=== сегмент данных ========================================================== section '.data' data readable writeable _class db 'FASMWIN32',0 ; Название собственного класса. Мы будем регистрировать свой. _title db 'Win32 text output',0 ; Текст в заголовке окна. _error db 'Startup failed.',0 ; Текст ошибки _text db 'Бывают иные встречи, совершенно даже с незнакомыми нам людьми, которыми мы начинаем интересоваться с ' db 'первого взгляда, как-то вдруг, внезапно, прежде чем скажем слово. Такое точно впечатление произвел на ' db 'Раскольникова тот гость, который сидел поодаль и походил на отставного чиновника. Молодой человек ' db 'несколько раз припоминал потом это первое впечатление и даже приписывал его предчувствию. Он беспрерывно ' db 'взглядывал на чиновника, конечно, и потому еще, что и сам тот упорно смотрел на него, и видно было, что тому ' db 'очень хотелось начать разговор. На остальных же, бывших в распивочной, не исключая и хозяина, чиновник смотрел ' db 'как-то привычно и даже со скукой, а вместе с тем и с оттенком некоторого высокомерного пренебрежения, как бы на ' db 'людей низшего положения и развития, с которыми нечего ему говорить. Это был человек лет уже за пятьдесят, ' db 'среднего роста и плотного сложения, с проседью и с большою лысиной, с отекшим от постоянного пьянства желтым, ' db 'даже зеленоватым лицом и с припухшими веками, из-за которых сияли крошечные, как щелочки, но одушевленные красноватые ' db 'глазки. Но что-то было в нем очень странное, во взгляде его светилась как будто даже восторженность, — пожалуй, был и ' db 'смысл и ум, — но в то же время мелькало как будто и безумие. Одет он был в старый, совершенно оборванный черный фрак, ' db 'с осыпавшимися пуговицами. Одна только еще держалась кое-как, и на нее-то он и застегивался, видимо желая не удаляться ' db 'приличий. Из-под нанкового жилета торчала манишка, вся скомканная, запачканная и залитая. Лицо было выбрито, по-чиновничьи, ' db 'но давно уже, так что уже густо начала выступать сизая щетина. Да и в ухватках его действительно было что-то солидно-чиновничье. ' db 'Но он был в беспокойстве, ерошил волосы и подпирал иногда, в тоске, обеими руками голову, положа продранные локти на залитый и липкий стол.',0 wHMain dd ? ; дескриптор окна pHMain dd ? ; дескриптор контекста устройства wc WNDCLASS ; Структура окна. Для функции RegisterClass msg MSG ; Структура системного сообщения, которое система посылает нашей программе. Структура содержится в INCLUDE\EQUATES\USER32.INC pnt PAINTSTRUCT ; Структура отрисовки клиентской области окна. rect RECT ; Структура ;=== таблица импорта ========================================================= section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL',user32,'USER32.DLL',gdi32,'GDI32.DLL' include 'api\kernel32.inc' include 'api\user32.inc' include 'api\gdi32.inc' |
16
. Члену style структуры wc
класса мы присваиваем комбинированное значение констант CS_HREDRAW и CS_VREDRAW (объединяя их операцией ИЛИ). Эти значения предписывают произвести перерисовку всего окна (созданного на основе данного класса) при его перемещении либо изменении ширины и высоты в ответ на изменение размера. При экспериментах без использования данных констант у меня часто возникали ситуации, в которых при обновлении недействительных областей окна отрисовывалась только часть окна, уродуя строки текста.Код обработчика сообщения WM_PAINT начинается у нас со строки 66
. Перво-наперво в обработчике у нас вызывается функция BeginPaint, которая возвращает структуру pnt типа PAINTSTRUCT
, содержащую в одном из своих членов координаты недействительной области. Эти координаты помогают выяснить, всю ли клиентскую область окна надо отрисовывать или только часть. Состав структуры pnt содержит информацию об отрисовке:
1 2 3 4 5 6 7 8 |
struct PAINTSTRUCT hdc dd ? fErase dd ? rcPaint RECT fRestore dd ? fIncUpdate dd ? rgbReserved db 32 dup (?) ends |
Описание параметров:
Наименование | Определение |
---|---|
hdc | Описатель (дескриптор) контекста устройства, в котором требуется перерисовка. То же самое значение, что и возвращаемое функцией в регистре eax. |
fErase | Необходимость затирания (очистки) фона окна в области, подлежащей обновлению.
|
rcPaint | Структура типа RECT , определяющая координаты недействительного прямоугольника, который требуется перерисовать. Координаты: left (x0), top (y0), right (x1), bottom (y1). Используя конкретный дескриптор контекста устройства (он же и в hdc), Вы не сможете рисовать вне этого недействительного прямоугольника. Значения координат недействительного прямоугольника даются относительно левого верхнего угла рабочей области окна. |
Затем, в строках 69
и 70
у нас присутствует пара функций SetBkColor, SetTextColor, которые (соответственно) задают цвет фона и символов для текущего контекста устройства. В качестве цвета в GDI-функциях используется RGB-палитра размерностью 16 млн. Напомню, что в данном конкретном примере для отрисовки текста у нас используется функция DrawText, которая подразумевает вывод текста в заданный прямоугольник с форматированием. Какой (извините за мой французский) прямоугольник? Зачем тут потребовался еще какой-то прямоугольник, когда вывод вроде бы должен и так производиться в прямоугольник, который представляет собой клиентскую области окна? Ничего не поделаешь, такая вот особенность функции DrawText, которая сперва выводит некий невидимый (виртуальный) ограничивающий прямоугольник, а только потом в нем рисует текст. Конечно эта внутренняя связка отрабатывает гораздо быстрее, чем это производилось бы в случае вызова двух функций. Соответственно, в коде нашего приложения, перед использованием функции DrawText, нам необходимо сформировать прямоугольник либо вручную, либо при помощи специализированной функции GetClientRect. Функция GetClientRect возвращает координаты клиентской области текущего окна, которые определяют верхний левый и нижний правый углы прямоугольника. После вызова, функция заполняет структуру rect типа RECT
, представляющую из себя следующее:
1 2 3 4 5 6 |
struct RECT left dd ? ; x-координата верхнего левого угла top dd ? ; y-координата верхнего левого угла right dd ? ; x-координата правого нижнего угла bottom dd ? ; y-координата правого нижнего угла ends |
Члены left (лево) и top (верх) инициализируются значением 0
, а right (право) и bottom (низ) принимают значения ширины и высоты полной клиентской области окна, к которому применена функция. Думаю, что можно и обойтись без данной функции, но тогда нам каким-то образом (вручную или другой функцией) придется сформировать структуру rect, которая используется в функции DrawText, поскольку с не подготовленной должным образом структурой, она попросту может нарисовать вне видимой зоны, то есть мы результатов в окне не увидим.
Далее, в строке 72
расположена сама функция DrawText. Отмечу, что у данной функции есть ряд преимуществ перед иными функциями работы с текстом. Например, нет необходимости вычислять длину строку (как в случае с TextOut), достаточно всего-лишь указать значение параметра -1 и выведена будет вся строка до завершающего 0
. Основное преимущество этой функции по отношению к TextOut, это возможность работать с дополнительными атрибутами, позволяющими нужным образом форматировать многострочные поля текста. Однако, это является одновременно и минусом, поскольку из-за дополнительного форматирования, функция DrawText медленнее своих аналогов. Помимо этого у функции имеется еще один небольшой недостаток, и состоит он как раз в необходимости предварительной инициализации ограничивающего прямоугольника (настройка которого иногда доставляет дополнительные неудобства). В нашем случае у функции задается метод форматирования текста DT_WORDBREAK, который подразумевает перенос не вмещающейся в прямоугольник строки в точке разрыва (пробела). В строке 73
мы вызываем функцию EndPaint, которая завершает отрисовку и освобождает контекст устройства. Ну а результатом работы нашей с вами программы является вот что:
Как мы видим, большой блок текста прекрасно "вписался" в клиентскую область окна.
Некоторые советы по работе с функцией DrawText:
- По умолчанию текст, который не вмещается в прямоугольник, заданный структурой rect, обрезается по правому краю. Дабы этого не происходило, используйте константу DT_NOCLIP;
- Если Вам потребовалось вывести текст в несколько строк, или перенести длинную строку, используйте константу DT_WORDBREAK. Без неё текст, в любом случае, будет выводиться в одну строку, не смотря на наличие в строке управляющих символов (перевод строки(LF), возврат каретки(CR)).
- Вертикальное выравнивание выводимого текста по центру с применением DT_VCENTER работает только для однострочного текста, поскольку должен использоваться только совместно с DT_SINGLELINE. Для полноценного центрирования многострочного текста необходимо писать собственный алгоритм: вычислить размер rect, и, меняя верхнюю и нижнюю координаты, добиться желаемого результата.
Пример 2: Вывод текста в окно с использованием функции TextOut
Теперь давайте рассмотрим еще один пример в рамках метода вывода текста в окно в обработчике сообщения WM_PAINT. На этот раз мы будем использовать, другую, более популярную, функцию для работы с текстовой информацией в окне под названием TextOut. Если выбирать между использованием из доступных функций работы с текстом, то очевидное преимущество функции TextOut заключается в простом задании координат стартовой позиции вывода строки, которые передаются непосредственно через входные параметры функции и отсчитываются относительно клиентской области окна. Из всех этих особенностей проистекает простота использования функции в коде, а основным недостатком является меньшая гибкость. Давайте, для начала, опишем общий алгоритм для приложения:
- В обработчике сообщения WM_CREATE: Создать новый объект шрифта при помощи функции CreateFont на основе существующего системного;
- В обработчике сообщения WM_PAINT: Получить указатель (дескриптор) на контекст устройства при помощи функции BeginPaint;
- В обработчике сообщения WM_PAINT: Произвести выбор объекта шрифта при помощи функции SelectObject на созданный нами;
- В обработчике сообщения WM_PAINT: Произвести рисование в клиентской области при помощи функции TextOut;
- В обработчике сообщения WM_PAINT: Произвести восстановление изначального объекта шрифта при помощи функции SelectObject;
- В обработчике сообщения WM_PAINT: Освободить указатель (дескриптор) на контекст устройства при помощи функции EndPaint;
- В обработчике сообщения WM_DESTROY: Удалить созданный нами объект шрифта при помощи функции DeleteObject;
Традиционно, как и в примере выше, за основу был взят исходный текст шаблона оконного приложения на ассемблере, затем слегка модифицирован. Изменения отмечены в исходном тексте цветом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
format PE GUI 4.0 ; Формат PE. Версия GUI 4.0. entry start ; Точка входа include '%include%\win32a.inc' ; Подключаем стандартный инклюд. Зачем? _style equ WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU ; ручное задание стиля. ЭКВИВАЛЕНТЫ должны задаваться ДО основного кода _bk_color equ (255 or (255 shl 8)) or (255 shl 16) ; цвет фона (три компоненты RGB) _text_color equ 0 ; цвет текста = черный ;=== сегмент кода ============================================================ section '.text' code readable executable start: invoke GetModuleHandle,0 ; Получим дескриптор приложения. Параметр = 0 - значит вернуть дескриптор вызвавшего функцию модуля. mov [wc.hInstance],eax ; Сохраним дескриптор в структуру wc mov [wc.style], CS_HREDRAW or CS_VREDRAW ; Стили перерисовки окна целиком invoke LoadIcon,0,IDI_ASTERISK ; Загружаем иконку IDI_ASTERISK из ресурсов исполняемого файла mov [wc.hIcon],eax ; Сохраним в структуре окна invoke LoadCursor,0,IDC_ARROW ; Загружаем курсор IDC_ARROW из ресурсов исполняемого файла mov [wc.hCursor],eax ; Сохраним дескриптор курсора в структуре окна mov [wc.lpfnWndProc],WindowProc ; Зададим ссылку на нашу процедуру обработки окна mov [wc.lpszClassName],_class ; Зададим имя класса окна mov [wc.hbrBackground],COLOR_WINDOW+1 ; Зададим цвет кисти invoke RegisterClass,wc ; Зарегистрируем наш класс окна test eax,eax ; Проверим eax на ошибку (0). jz error ; Если 0 - то ошибка. Выводим и выходим. invoke CreateWindowEx,0,_class,_title,_style,128,128,512,512,NULL,NULL,[wc.hInstance],NULL ; Создадим экземпляр окна на основе зарегистрированного класса. в eax возвращает дескриптор окна. test eax,eax ; Проверим eax на ошибку (0). jz error ; Если 0 - то ошибка. Выводим и выходим. mov [wHMain],eax ; сохраним дескриптор окна ;--- цикл обработки сообщений ------------------------------------------------ msg_loop: invoke GetMessage,msg,NULL,0,0 ; Получаем сообщение из очереди сообщений приложения, помещает его в структуру msg. Результат выполнения в eax. or eax,eax ; Сравнивает eax с 0 jz end_loop ; Если 0 то пришло сообщение WM_QUIT - выходим из цикла ожидания сообщений, если не 0 - продолжаем обрабатывать очередь msg_loop_2: invoke TranslateMessage,msg ; Дополнительная функция обработки сообщения. Конвертирует некоторые сообщения и отправляет их обратно в очередь. Служит для обработки клавиатуры. invoke DispatchMessage,msg ; Пересылает сообщения соответствующим процедурам обработки сообщений (WindowProc ...). jmp short msg_loop ; В начало цикла error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK ; Вывод сообщения об ошибке end_loop: invoke ExitProcess,[msg.wParam] ; Выход из программы. Код выхода сохраняется в msg.wParam. Его надо передать системе ;--- процедура обработки сообщений окна (функция окна, оконная процедура) ---- proc WindowProc hWnd,wMsg,wParam,lParam push ebx esi edi ; сохраним все регистры cmp [wMsg],WM_DESTROY ; Проверим на WM_DESTROY je .wmdestroy ; на обработчик wmdestroy cmp [wMsg],WM_CREATE ; Проверим на WM_CREATE je .wmcreate ; на обработчик wmcreate cmp [wMsg],WM_PAINT ; Проверим на WM_PAINT je .wmpaint ; на обработчик wmpaint .defwndproc: invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam] ; Дефолтная функция. Обрабатывает сообщения, которыми наша программа не пользуется. jmp .finish .wmcreate: invoke CreateFont,24,16,0,0,400,FALSE,FALSE,FALSE,DEFAULT_CHARSET,OUT_RASTER_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,FIXED_PITCH+FF_DONTCARE+FF_ROMAN,NULL or eax,eax jz .cffailed mov [fHMain],eax ; сохраним дескриптор нового шрифта xor eax,eax jmp .finish .cffailed: or eax,-1 jmp .finish .wmpaint: invoke BeginPaint,[hWnd],pnt mov [pHMain],eax ; Сохранить контекст (дескриптор) устройства invoke SelectObject, [pHMain], [fHMain] ; Выберем объект созданного шрифта mov [ofHMain],eax ; сохраним дескриптор старого объекта шрифта invoke lstrlen,_text ; Вычислим длину выводимой строки invoke TextOut,[pHMain],0,0,_text,eax ; Выведем текст в клиентскую область окна invoke SelectObject, [pHMain], [ofHMain] ; Восстанавливаем предыдущий объект шрифта invoke EndPaint,[hWnd],pnt ; Освободить контекст устройства xor eax,eax ; возвратим 0 в случае успешного завершения jmp .finish .wmdestroy: ; Обработчик сообщения WM_DESTROY. Обязателен. invoke DeleteObject,[fHMain] ; Удаляем созданный нами объект шрифта invoke PostQuitMessage,0 ; Посылает сообщение WM_QUIT в очередь сообщений, что вынуждает GetMessage вернуть 0. Посылается для выхода из программы. Посылается только основным окном. xor eax,eax ; Если наша процедура окна обрабатывает какое-либо сообщение, то она должна вернуть 0. eax=0. Иначе программа поведет себя непредсказуемо. .finish: pop edi esi ebx ; восстановим все регистры ret endp ;=== сегмент данных ========================================================== section '.data' data readable writeable _class db 'FASMWIN32',0 ; Название собственного класса. Мы будем регистрировать свой. _title db 'Win32 text output',0 ; Текст в заголовке окна. _error db 'Startup failed.',0 ; Текст ошибки _text db 'Тестовая строка',0 ; Строка для вывода wHMain dd ? ; дескриптор окна pHMain dd ? ; дескриптор контекста устройства fHMain dd ? ; дескриптор нового объекта шрифта ofHMain dd ? ; дескриптор предыдущего объекта шрифта wc WNDCLASS ; Структура окна. Для функции RegisterClass msg MSG ; Структура системного сообщения, которое система посылает нашей программе. Структура содержится в INCLUDE\EQUATES\USER32.INC pnt PAINTSTRUCT ; Структура отрисовки клиентской области окна. rect RECT ; Структура ;=== таблица импорта ========================================================= section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL',user32,'USER32.DLL',gdi32,'GDI32.DLL' include 'api\kernel32.inc' include 'api\user32.inc' include 'api\gdi32.inc' |
В данном примере мы, как и прежде, производим вывод в окно текстовой строки, однако основное отличие заключается в используемом для вывода шрифте. Оригинальный код у нас начинается со строки 64
, которая относится к обработчику сообщения WM_CREATE. Однако, причем тут сообщение WM_CREATE, если мы в рамках метода должны делать всё в обработчике WM_PAINT? Действительно, проще было бы сгруппировать все функции в обработчике WM_PAINT, но я решил немного оптимизировать код обработчика данного сообщения по производительности. Задайтесь вопросом, зачем нам каждый раз создавать/уничтожать объект шрифта в интенсивно используемом обработчике отрисовки, загружая процессор совершенно ненужными тактами и тормозя процесс? К тому же, модификация не нарушает принципа метода: вся отрисовка по-прежнему производится внутри обработчика сообщения WM_PAINT, просто некоторые подготовительные и заключительные действия вынесены (за скобки) в обработчики других сообщений: создание объекта шрифта в обработчик WM_CREATE, а удаление объекта созданного нами ранее шрифта в обработчик WM_DESTROY. Итак, в момент создания окна поступает сообщение WM_CREATE, и первое что мы делаем - вызываем функцию создания объекта логического шрифта (строка 64
).
Как следует из названия используемой нами функции CreateFont, она создает объект логического шрифта путем видоизменения существующего системного шрифта на основе заданных входных параметров. В последствии созданный объект может быть использован в коде приложения. На параметрах данной функции хотелось бы остановиться подробнее, поскольку на сайте Microsoft не сильно озадачиваются пояснениями. Соответствие параметров: слева - направо.
Параметр | Описание |
---|---|
nHeight | Желаемая высота (символов) шрифта в логических единицах. |
nWidth | Желаемая ширина (символов) шрифта в логических единицах. |
nEscapement | Угол наклона опорной (базовой) линии текста по отношению к горизонтальной оси (в десятых долях градуса). Для простоты понимания стоит отметить, что изменение параметра позволяет выводить строку текста под углом к базовой горизонтальной линии (например, вертикально). |
nOrientation | Угол наклона символов в знакоместе относительно базовой горизонтальной линии (в десятых долях градуса). Параметр позволяет изменять наклон (поворачивать) самих символов в знакоместе вне зависимости от наклона самой строки. |
fnWeight | Жирность (насыщенность) шрифта. |
fdwItalic | Курсив |
fdwUnderline | Подчеркнутость. |
fdwStrikeOut | Перечеркнутость. |
fdwCharSet | Кодировка. Набор символов, используемый для кодирования текста. |
fdwOutputPrecision | Точность шрифта или критерий соответствия. Насколько точно на стадии подбора создаваемый логический шрифт соответствует имеющимся в системе физическим шрифтам. |
fdwClipPrecision | Точность прилегания шрифта. Метод изменения отображения (отсечения) части символа, попавшей за пределы региона отсечения, то есть не попадающей в видимую область. |
fdwQuality | Качество вывода глифов. |
fdwPitchAndFamily | Семейство шрифта. |
lpszFace | Имя гарнитуры (название) шрифта. Указатель на строку символов, содержащую название семейства шрифтов и заканчивающуюся нулем. |
Семейство создаваемого шрифта можно задавать несколькими способами: через константу входного параметра fdwPitchAndFamily, либо через указание параметра lpszFace. В строке 66
выполняется контроль выполнения функции, проверяется выходное значение в регистре eax, и если оно нулевое (0, NULL), что означает ошибку, то происходит выход из оконной процедуры с выставлением значения на выходе равного -1
. Можно и не создавать собственный логический шрифт, а использовать набор встроенных шрифтов, дескрипторы которых запрашиваются для загрузки в контекст функцией GetStockObject. Обработчик сообщения WM_PAINT начинается со строки 74
. Перво-наперво вызывается у нас функция BeginPaint для получения координат недействительной области (в структуре pnt
) и сохранения дескриптора контекста устройства (в переменной pHMain, строка 75
). Затем у нас в коде, в строке 76
, появляется не встречающаяся нам ранее функция SelectObject, которая предназначается для выбора объекта нового шрифта.
Для этого определения потребуется пояснение. Для каждого контекста устройства в произвольный момент времени может существовать один единственный активный GDI-объект каждого типа. То есть, вы можете иметь активными для того или иного контекста только одну кисть, одно перо, один шрифт и так далее. Когда Вы вызываете функцию SelectObject, вы делаете активным объект какого-либо типа. При этом, функция возвращает предыдущий дескриптор объекта данного типа, для облегчения переключения между объектами и упрощения восстановления контекста в прежнее состояние по окончании обработки сообщения.
Другими словами, рекомендовано восстанавливать контекст устройства в оригинальное состояние, в котором он находился до различных манипуляций по переключению объектов в нашем коде. Если вы меняете какие-либо из GDI-объектов, вы должны восстановить значения на оригинальные до возврата из обработчика сообщения. Возвращаемое функцией SelectObject значение является: если выбранный объект не регион - дескриптором замененного объекта, если выбранный объект является регионом - одним из значений SIMPLEREGION, COMPLEXREGION, NULLREGION. Фактически, функция SelectObject возвращает описатель заменённого (предыдущего) объекта того же типа для конкретного контекста устройства.
Функция TextOut имеет один небольшой недостаток: в одном из входных параметров необходимо задавать длину выводимой строки. Для вычисления длины строки используется несколько подходов, которые подробно описаны в этой вот статье. В нашем примере (в строке 78
) для вычисления используется системная функция lstrlen, возвращающая длину строки в регистре eax
(без учета нуль-терминатора), а непосредственно за ней вызывается функция TextOut, выводящая строку в окно. В строке 80
у нас повторно используется функция SelectObject для восстановления значения объекта шрифта контекста устройства. После вызова завершающей функции EndPaint, код обработчик сообщения WM_PAINT фактически заканчивается.
Результатом работы нашего приложения является вывод на экран окна со следующим содержимым:
В обработчике сообщения WM_DESTROY (строка 84
), непосредственно перед закрытием основного окна программы и выходом из неё, мы выполняем удаление созданного нами объекта нового шрифта. Опять же, стоит отметить, что по сравнению с эталонным шаблоном, в данном примере (строка 113
) у нас подключается новая библиотека gdi32.dll для обеспечения работы с GDI-функциями.
Метод 2: рисование в обработчиках других сообщений окна или в обработчике сообщений потока
Данный метод носит название мгновенный. Как у разработчика под Windows достаточно "зеленого", у меня отсутствует опыт программирования всех возможных вариантов вывода текста в окно на ассемблере, однако я могу предположить, что в практике программиста встречаются ситуации, когда требуется изменить содержимое окна не в обработчике сообщения WM_PAINT, а в каком-либо другом месте приложения. Примером могут служить программы, интенсивно работающие с графикой: демки, игры и подобные. В таких приложениях часто используется полноэкранный режим, поэтому на сообщения WM_PAINT надежды мало, тут требуется непрерывный потоковый вывод. Другими словами, существуют категории приложений, в которых вывод текста (как разновидности графического объекта) организован несколько иначе чем то, что мы уже изучили, и может производиться в различных обработчиках событий окна или потока. Поэтому все зависит конкретной задачи и многих нюансов реализации приложения, отсюда следует немаловажный вывод:
Ну например, не можем мы ждать сообщение WM_PAINT для отрисовки какого-либо объекта, когда оно еще придет не понятно, а в некоторых случаях вообще может не поступать длительное время, а тот же монстр в игре перемещается постоянно и ему как-то все-равно когда у нас что приходит, ему надо двигаться через равные интервалы, непрерывно.
В случае использования мгновенного метода вывода у нас имеется следующий путь решения задачи:
- Код в обработчике сообщений окна (отличных от WM_PAINT) или вне обработчика (в очереди сообщений потока), должен использовать контейнер из пары функций GetDC...ReleaseDC либо GetWindowDC...ReleaseDC.
Основной плюс данного метода заключается в том, что чуть легче настроить рисование во всей рабочей области окна, а не только в недействительной его части, а минус заключается в необходимости самостоятельной валидации недействительной области (что бы постоянно не создавались сообщения WM_PAINT), при помощи специализированных функций, потому как ReleaseDC этого не делает.
Пример в контексте данного метода отрисовки у нас построен так, что частичное (мгновенное) рисование осуществляется не в обработчиках сообщений окна, а выполняется в теле цикла обработки сообщений потока. В качестве иллюстрации к данному методу получилась эдакая миниатюрная "демка", которая выводит в окно строку, циклически перемещающуюся по экрану сверху вниз. Давайте, сперва, более детально распишем алгоритм работы примера:
- В обработчике сообщения потока: получить контекст устройства при помощи функции GetDC;
- В обработчике сообщения потока: по заданным координатам в окне произвести отрисовку символа при помощи функции TextOut;
- В обработчике сообщения потока: перед выходом из программы освободить указатель (дескриптор) на контекст устройства при помощи функции ReleaseDC;
а теперь приведем непосредственно сам код приложения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
format PE GUI 4.0 entry start include '%include%\win32a.inc' _style equ WS_POPUP+WS_VISIBLE _color equ 0000FF00h ;=== сегмент кода ============================================================ section '.text' code readable executable start: invoke GetModuleHandle,0 mov [wc.hInstance],eax mov [wc.style], CS_HREDRAW or CS_VREDRAW invoke LoadIcon,0,IDI_ASTERISK mov [wc.hIcon],eax invoke LoadCursor,0,IDC_ARROW mov [wc.hCursor],eax mov [wc.lpfnWndProc],WindowProc mov [wc.lpszClassName],_class mov [wc.hbrBackground],2 invoke RegisterClass,wc test eax,eax jz init_error invoke CreateWindowEx,0,_class,_title,_style,100,100,200,200,NULL,NULL,[wc.hInstance],NULL test eax,eax jz init_error mov [wHMain],eax invoke GetDC,eax mov [pHMain],eax invoke SetBkColor,[pHMain],2 invoke SetTextColor,[pHMain],_color ;--- цикл обработки сообщений ------------------------------------------------ msg_loop: invoke PeekMessage,msg,NULL,0,0,PM_NOREMOVE or eax,eax jz no_message invoke GetMessage,msg,NULL,0,0 or eax,eax jz end_loop msg_loop_2: invoke TranslateMessage,msg invoke DispatchMessage,msg jmp short msg_loop no_message: invoke TextOut,[pHMain],[X],[Y],_string,26 inc dword [Y] cmp [Y],200 jng @f mov [Y], -10 @@: invoke Sleep,10 jmp msg_loop init_error: invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK end_loop: invoke ReleaseDC,[wHMain],[pHMain] invoke ExitProcess,[msg.wParam] ;--- процедура обработки сообщений окна (функция окна, оконная процедура) ---- proc WindowProc hWnd,wMsg,wParam,lParam push ebx esi edi cmp [wMsg],WM_DESTROY je .wmdestroy .defwndproc: invoke DefWindowProc,[hWnd],[wMsg],[wParam],[lParam] jmp .finish .wmdestroy: invoke PostQuitMessage,0 xor eax,eax .finish: pop edi esi ebx ret endp ;=== сегмент данных ========================================================== section '.data' data readable writeable _class db 'FASMWIN32',0 _title db 'Win32 text output',0 _error db 'Startup failed.',0 _string db 'abcdefghijklmnopqrstuvwxyz',0 X dd 6 Y dd -10 wHMain dd ? pHMain dd ? wc WNDCLASS msg MSG ;=== таблица импорта ========================================================= section '.idata' import data readable writeable library kernel32,'KERNEL32.DLL',user32,'USER32.DLL',gdi32,'GDI32.DLL' include 'api\kernel32.inc' include 'api\user32.inc' include 'api\gdi32.inc' |
Как можно заметить после изучения кода, логика у нас действительно существенно отличается от предыдущих примеров данной статьи: вывод объектов в окно происходит без остановки, постоянно, как говорится, без какого-либо пользовательского участия. Кстати, по похожей схеме функционируют разнообразные игры и демки. После создания окна, при помощи функции GetDC (строка 32
) мы запрашиваем описатель контекста устройства (DC) для клиентской области заданного окна, при этом сохраняя его в переменную. Затем, уже в самом обработчике сообщений потока у нас используется новая пока для нас функция PeekMessage (строка 40
), которая проверяет очередь потока без ожидания (поступления сообщения), и в случае отсутствия оного (ведь обычно мы ничего не делаем с коном, просто смотрим демку) передает управление на метку no_message, где как раз и происходит вывод строки при помощи функции TextOut (строка 51
). Таким вот нехитрым алгоритмом создается непрерывный вывод в окно, думаю Вам будет полезна эта реализация при создании собственных демок/игр. При попытке закрыть окно по комбинации Alt-F4, производится освобождение дескриптора контекста устройства при помощи функции ReleaseDC (строка 62
).
Метод 3: Создание собственного контекста
Используются функции CreateDC, CreateCompatibleDC, DeleteDC. При использовании данного способа, так же, требуется валидировать недействительную область самостоятельно.
Правила (он же не разобранный мусор)
Итак, давайте ка сведем все требования в одно место:
- Для начала отображения вы должны получить контекст устройства при помощи функции, зависящей от способа реакции.
- Непосредственно перед выполнением действий по выводу информации в клиентскую область, приложение должно определить её размерность.
- Приложение должно обеспечивать перерисовку внутренней поверхности (или части) любого из собственных окон в любой момент времени при получении сообщения WM_PAINT.
- Если Вы в своём приложении не обрабатываете сообщение WM_PAINT, требуется по крайнем мере вызвать функцию DefWindowProc либо ValidateRect для подтверждения недействительной области, в противном случае Windows будет непрерывно слать сообщения WM_PAINT вашему окну.
- Если Вы в своём приложении обрабатываете сообщение WM_PAINT, но при этому не используете пару функций BeginPaint/EndPaint, то необходимо вызвать функцию ValidateRect для подтверждения недействительной области, в противном случае Windows будет непрерывно слать сообщения WM_PAINT вашему окну.
- После того, как Вы закончили работу с дескриптором контекста устройства, Вы должны освободить его до завершения кода обработки сообщения, то есть до окончания обработки сообщения. Не рекомендуется получать дескриптор в ответ на одно сообщения, а освобождать в ответ на другое!
- При использовании контейнера функций BeginPaint/EndPaint, система автоматически ограничивает рисование вне области обновления, другими словами вы не сможете что-нибудь нарисовать вне области обновления. Поэтому при использовании BeginPaint/EndPaint стоит это помнить. Информация, нарисованная на действительной области, просто не будет отображена на экране.
- В параметрах функций API Windows, отвечающих за обработку текста, нет стандартной поддержки выравнивания многострочного текста по ширине.
- В процессе перерисовки генерируется еще сообщение WM_ERASEBKGND (когда именно?). Как правило его не обрабатывают, поэтому оно передается функции DefWindowProc, которая в ответ перерисовывает фон соответствующей (недействительной?) области окна (используя кисть, определенную при регистрации класса окна). Последовательность со стороны системы тут следующая: перед записью в очередь сообщений приложения сообщения WM_PAINT, система посылает процедуре окна сообщение WM_ERASEBKGND. Если процедура окна не имеет обработчика сообщения WM_ERASEBKGND, попросту передавая его умолчальной функции DefWindowProc, последняя в ответ на данное сообщение закрашивает внутреннюю область окна с использованием кисти, указанной в соответствующем классе окна (задается при регистрации класса окна). Из этого следует, что если оконная процедура нарисует что-либо в окне во время обработки других сообщений (отличных от WM_PAINT), после прихода первого же сообщения WM_PAINT нарисованное изображение будет закрашено.
- Индивидуальные вызовы различных функций (DrawText, TextOut, PolyTextOut и прочих) не вызывают сиюминутного изменения в клиентской области. Вместо этого ядро системы буферизирует (накапливает) запросы отрисовки, и только по вызову функции EndPaint или другой, освобождающей контекст, система выполняет обновление клиентской части окна.
- Функции по работе с текстом не имеют входных параметров, определяющих шрифт, размер букв, цвет фона, цвет букв и прочих настроек стиля отображения. Подобные атрибуты текста хранятся в соответствующих членах структуры контекста устройства. Поэтому, для переключения этих характеристик необходимо использовать специальные функции создания и переключения объектов контекста.
Спасибо! Почерпнул много полезной информации