Ассемблер: вывод текста в окно

Метки:  , , , , , , , , , , ,

Вывод текста в окно, как способ визуализации результатов работы приложения, является одной из основных задач в любом языке программирования. На пути изучения программирования на языке Ассемблера под Windows, очередным шагом является освоение принципов работы со строковой информацией в рамках использования объектов графического интерфейса (окон). В последние десятилетия человечество заметно изменило механизмы взаимодействия пользователя с операционной системой в сторону использования графики, постепенно нивелируя роль текстовых режимов работы, объясняя это банальным удобством. Тем не менее, в области использования современных графических интерфейсов, мы так и не отошли от основной формы выражения языка - письменности, потому как текст был и остается одним из превалирующих типов информации. А начиналось всё со старого-доброго *nix'а, где текстовая консоль выполняла основную задачу по взаимодействию с пользователем. Да и не смотря на тотальное засилие графики во основных пользовательских операционных системах (OSx/Android/Windows), текстовый режим в виде практически не потерявшей свой первоначальный вид консоли, всё еще присутствует в составе перечисленных операционных систем. Предшественницей Windows была система MSDOS, в которой интерфейс функционировал в текстовом режиме. Помните, как в ней реализовывался вывод текста на ассемблере, который осуществлялся через функции прерывания 10h BIOS, либо попросту прямой записью данных в область видеопамяти (PC: сегмент B800h).

Всё было довольно просто и достаточно увлекательно, ведь экран можно было представить в виде телетайпа, а вывод отождествлялся с потоком символов, автоматически "заворачивающемся" (переносящемся) при окончании очередной строки. Видеопамять отображалась на физический дисплей видеоадаптера таким образом, что можно было не заботиться о переносе строк как посредством прямого вывода в видеопамять, так и через функции прерывания 10h. Во времена низкоуровневого программирования вывода под MSDOS, проблемы существовали, но большинство из них касались графических режимов (специфическая настройка под тип адаптера (CGA, VGA, SVGA), особенности конкретной видеокарты и прч.), и все они были решаемы. Текстовый режим представлял собой матрицу ячеек фиксированного размера (8x8, 8x14, 8x16, ...), что существенно упрощало создание объектов любой сложности, начиная от одиночных символов и заканчивая полнофункциональными оконными интерфейсами. Другой стороной простоты вывода текста на ассемблере в реальном режиме работы процессора (для которого было написано большинство программ под MSDOS), было фактическое отсутствие необходимости делить экран между несколькими приложениями, то есть "в экран" мог беспрепятственно писать любой код, выполнявшийся в системе в данный момент времени. Да, мир тогда был совершенно иным и мы попросту к нему привыкли.
А потом наступил Windows. Нельзя сказать, что он неожиданно постучался в двери бытия, скорее параллельно существовал, понемногу обращая на себя все более пристальное внимание сообщества, в том время как DOS пыталась удержать первенство при помощи различного рода расширителей DOS (DOS/4GW и подобные). И хотя Windows ранних версий (3.11/95/98/98se/Me) представляла собой всего-лишь графическую надстройку над DOS, она уже тогда была ориентирована на обеспечение псевдомногозадачности, в ней планировалось разделение общих аппаратных ресурсов станции между множеством "одновременно" выполняющихся приложений. К тому же, основное (в некоторых версиях излишнее) внимание в Windows было уделено использованию оконного графического интерфейса, функционал которого обеспечивался так называемым Интерфейсом графического устройства (GDI).

GDI (Интерфейс графического устройства, Graphics Device Interface, Graphical Device Interface) — подсистема ядра, являющаяся одной из составных частей пользовательского интерфейса (оконный менеджер) Microsoft Windows. Это интерфейс Windows, который формирует графические объекты (примитивы: точки/линии/кривые/символы), шрифты и палитры, и передает их на устройства отображения (монитор, принтер и прч.).

По описанным выше причинам, экран в Windows это уже среда, разделенная между разнообразными "одновременно" функционирующими графическими приложениями. Логично предположить, что в многозадачных операционных системах должны действовать более строгие правила, дабы исключить ситуации, когда приложения используют для вывода своей информации чужую область экрана (окно/часть окна стороннего приложения). Поэтому, вывод текста в окно графического интерфейса Windows далеко не такая тривиальная задача, как её аналог для текстового режима в MSDOS, и тут нам необходимо усвоить ряд правил:

  • Текст в графическом пользовательском интерфейсе (GUI) Windows это не фиксированная по ширине и высоте матрица, начинающаяся с заранее определенных позиций (как это было в DOS). Текст может стартовать абсолютно в любой точке окна (имеет произвольные координаты X,Y), быть разного размера (высота/ширина), отображаться разнообразными шрифтами. Текст в графическом пользовательском интерфейсе - это объект графического пользовательского интерфейса, сгруппированный из пикселей, то есть картинка!. Каждый символ представлен набором пикселей (точек), которые объединены в единый рисунок.
  • Текстовая консоль помнит весь произведенный в неё вывод. Другими словами, текст на экране в текстовом режиме MSDOS мог оставаться сколь угодно долго вплоть до того момента, пока он не перезаписывался или не производились иные действия (смена режима). В Windows приложение должно само "помнить" весь вывод, произведенный в окно и быть готовым перерисовать содержимое окна, когда система посылает окну соответствующее сообщение.
  • Применительно к выводу в графическое окно считается что символы рисуются, а не печатаются.
  • Операционная система Windows скрывает от пользователя особенности реализации того или иного видеоадаптера. Таким образом принципы вывода унифицируются, больше нет необходимости заботиться о каких-то реализациях архитектуры, код везде выглядит одинаково.

Перед тем как перейти непосредственно к изучению концепции вывод текста в окно на ассемблере, давайте перечислим функции Win32 API, предназначающиеся для работы с текстом: TextOut, ExtTextOut, TabbedTextOut, DrawText, DrawTextEx, SetTextAlign, SetTextColor, PolyTextOut.

Области (регионы)

Фактически объектами любого современного графического интерфейса являются окна, фактически представляющие собой логические прямоугольники. Пользовательское приложение, как и система, оперирует понятием окна как некой прямоугольной областью, ограничивающей вывод информации. Окно в Windows разделено на несколько областей:

  • Рабочая область (working area) это окно целиком, то есть вся область, занимаемая окном на экране;
  • Неклиентская область (non-client area) это часть окна, включающая в себя служебные элементы: рамка (бордюр), заголовок, меню, кнопки масштабирования, закрытия и восстановления и полосы прокрутки. Система по умолчанию берет на себя управление большинством аспектов неклиентской области.
  • Клиентская область (client area) это оставшаяся часть окна, предназначенная для отображения собственной информации приложения (текст/графика). Например, текстовый редактор отображает документ в клиентской области главного окна. Пользовательское приложение отвечает за обработку клиентской области окна. Для того, чтобы контролировать клиентскую область окна (принимать ввод пользователя и отображать в ней информацию), приложению необходимо инициализировать оконную процедуру.

Клиентская область это фактически регион окна приложения, за обслуживание которого отвечает само приложение. За неклиентский регион (non-client area), бордюры, заголовок, ползунок, окна, отвечает ядро системы.

window client area

Дабы приложения не "портили" окна чужих приложений, по умолчанию Windows обеспечивает контроль за выводом посредством ограничения области отрисовки каждого окна только его собственной клиентской областью.

В большинстве случае, вся отрисовка в окне приложения выполняется в границах клиентской части окна.

Надо учитывать, что размер этой клиентской области окна не постоянен, поскольку пользовательская активность является причиной постоянных изменений атрибутов окна (размер, видимость и прч.). Вот именно это обстоятельство и является ключевым в понимании природы графического интерфейса. Теперь давайте поговорим о действительных и недействительных областях.

(Не)действительные области

В какой-то момент времени изображение (содержимое) окон всех приложений переносится системой в память видеоадаптера (видеопамять) для непосредственного отображения на экране. Начиная с этого момента подразумевается, что системе известно содержимое части экрана, соответствующее всем окнам, контекст которых был только что перенесен. Ядро постоянно отслеживает объекты графического интерфейса, расположенные на экране: изменения размеров окон, их перемещение. Если никаких изменений в окнах приложений не происходит, то видеоадаптер обновляет содержимое экрана, периодически выбирая его из собственной видеопамяти. Если окна приложений начинают открываться/закрываться/перемещаться, то на экране появляются области, информация об изображении которых теряет актуальность, иными словами потеряна (неизвестна) для операционной системы. В этом случае система извещает окна, области (части, фрагменты) которых стали недействительными, о необходимости восстановить (перерисовать) собственное содержимое. Ведь на рабочем столе зачастую возникают ситуации, когда пользователь перетаскивает существующее стороннее окно либо поверх содержимого нашего окна, затем закрывает/вновь перемещает его, или просто сворачивает и разворачивает окно приложения. Поскольку часть окна (или окно целиком) перекрыта содержимым находящегося выше окна, оригинальная часть замещена (скрыта) частью содержимого перекрывающего окна, поэтому часть клиентская области перекрытого окна становится недействительной. Давайте перечислим все состояния:

  • Недействительная (обновляемая) область (invalid rectangle, update region) - регион клиентской части окна приложения, содержимое которого потеряло актуальность и требует обновления (перерисовки).
  • Действительная (необновляемая) область (valid rectangle) - регион клиентской области окна приложения, графическая информация о котором известна операционной системе.

В зависимости от метода получения информации об области обновления окна, Windows определяет недействительную область как наименьший прямоугольник, целиком включающий в себя область, требующую обновления, либо в виде точной информации об области обновления (многоугольник), либо в полях структуры, имеющей прототип PAINTSTRUCT, возвращаемой функцией получения контекста.

Согласитесь, что было бы удобно, если бы система брала на себя всю работу по отрисовке недействительных областей окон, однако Windows не отвечает за сохранность содержимого окон при подавляющем большинстве операций (перекрытии, сворачивании и прч.), поскольку подобный механизм потребовал бы от системы обработки огромного количества данных, что привело бы к непрогнозируемому падению производительности.

В случае, когда система обнаруживает недействительную область в пределах клиентской части окна, она уведомляет приложение-владельца, посылая в его очередь сообщений сообщение WM_PAINT, которое сигнализирует о необходимости восстановить потерявшую актуальность область. Координаты недействительной области содержатся в дополнительных параметрах сообщения (структура pt типа POINT). В случае, когда в каком-либо окне оказывается сразу множество областей, подлежащих обновлению, в очередь сообщений приложения поступает только одно сообщение WM_PAINT, в котором определена область, включающая в себя все недействительные области.

К примеру, в обработчике сообщения WM_PAINT вставляют вызов функции BeginPaint, которая подтверждает (проверяет) недействительную область, дополнительно возвращая её координаты для дальнейшей работы с ними в коде приложения.

Вот с этого момента для меня всё начинает усложняться :) Получается как-то нетривиально, ведь алгоритм программы должен быть спроектирован таким образом, чтобы в любой момент оконная процедура могла перерисовать содержимое собственного окна (или часть окна). В связи с этим у нас появляются следующие правила:

  • Данные о всех состояниях перерисовываемого содержимого окна должны храниться в пользовательском приложении. Это существенно меняет подход к написанию приложений, однако это не такая уж и сверхзадача, поскольку реализовать хранение состояний выводимых в окно данных в переменных/буферах приложения не так уж и сложно.
  • Для упрощения алгоритма перерисовки содержимого (части) окна, весь вывод (информации в окно) настоятельно рекомендуется производить одном месте кода - в обработчике сообщения WM_PAINT оконной процедуры.
  • С целью увеличения скорости работы приложения следует обновлять только (недействительную) область окна, требующую обновления. Однако никто не запрещает обновлять окно целиком, просто надо учитывать, что в зависимости от "тяжести" содержимого окна, на подобную отрисовку могут потребоваться существенные производительные затраты.

Если честно, перед начинающим программистом встают не такие уж тривиальные задачи. Тут, как говорится, пока не пощупаешь все эти подходы на практике, принципа не поймешь. К примеру, если мы пишем эдакий простенький графический редактор, который предназначен для рисования мышью (абстрактная кривая) в основном окне. Получается, мы должны хранить все точки данной фигуры, потому как в случае необходимости мы должны будем отрисовать её всю целиком. Та же ситуация и с текстом, приложение должно хранить всю ту информацию, которая использовалась во всех элементах окна и на основании которой оно может в любое время окно обновить. При работе с текстовой информацией (выводе/вводе), код приложения должен сохранять данные в памяти, чтобы их можно было "по требованию" системы вывести на экран, тем самым обновив (перерисовав) актуальное содержимое окна. Таким образом, принимая во внимание всё вышеизложенное, можно сделать следующий вывод:

Приложение Windows принципиально отличается от консольной программы MSDOS тем, что хранит (и обрабатывает) одновременно начальные, промежуточные и конечные результаты своей работы. В противоположность программе MSDOS, которая обычно считывала данные и осуществляла вывод данных в процессе их последовательного поступления. Значит, программа Windows, в отличие от консольной программы, обладает "памятью прошлых периодов". И эту особенность необходимо заучить наизусть :).

Контекст устройства

Очевидно, что отрисовку можно производить внутри кода обработки любого входящего сообщения (чаще всего WM_PAINT), но разве это единственное условие? Конечно же нет. В коде процедуры обработки сообщения, непосредственно перед началом рисования чего-либо в клиентской области "собственного" окна, приложению необходимо запросить у системы так называемое "разрешение" на подобные действия. Это обусловлено тем, что в многозадачной системе Windows нет возможности прямого бесконтрольного вывода на экран (как это было во времена MSDOS), тут все должно подчиняться некоему регламенту. С этой целью в Windows имеются специальные функции, которые определяют размер клиентской области, шрифт, цвета и другие GDI-атрибуты и возвращают (предоставляют) приложению так называемый дескриптор контекста устройства (Device Context Descriptor), который иногда еще называют контекстом отображения. Поскольку контекст устройства (Device Context,DC) является одним из основополагающих понятий в понимании механизмов вывода информации на устройство, я постараюсь дать максимальное количество определений для углубления в тему:

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

Как вы впоследствии увидите, практически у всех GDI-функции вывода в Windows одним из входных параметров является контекст устройства. Дескриптор (описатель) контекста устройства, возвращаемый функциями API, это своеобразный указатель, предоставляемый системой пользовательскому уровню приложения, посредством которого приложение получает доступ к устройству вывода (например, экран). Каждое приложение в случае необходимости экранного вывода получает в свое распоряжение собственный контекст и осуществляет посредством него вывод изображения. Поэтому, более интуитивно дескриптор контекста устройства воспринимается в качестве указателя на некую системную структуру в памяти ядра, через которую система выводит данные на физическое устройство отображения. Надо учитывать, что контекст является объектом Windows, который, в свою очередь, содержит описатели объектов графических примитивов (характеристик), используемых при работе с контекстом:

Атрибут контекста Значение по умолчанию Функция для изменения Функция для получения
Режим отображения (Mapping mode) MM_TEXT SetMapMode GetMapMode
Начало координат окна (Window origin) (0,0) SetWindowOrgEx GetWindowOrgEx
Начало координат области вывода (Viewport Origin) (0,0) SetViewportOrgEx GetViewportOrgEx
Протяженность окна (Window extent) (1,1) SetWindowExtEx GetWindowExtEx
Протяженность области вывода (Viewport extent) (1,1) SetVievportExtEx, SetMapMode GetVievportExtEx
Перо (Pen) BLACK_PEN SelectObject SelectObject
Кисть (Brush) WHITE_BRUSH SelectObject SelectObject
Шрифт (Font) SYSTEM_FONT SelectObject SelectObject
Битовый образ (Bitmap) NOT SelectObject SelectObject
Текущая позиция пера (Current pen position) (0,0) MoveToEx, LineTo, PolylineTo, PolyBezierTo GetCurrentPositionEx
Режим фона (Background mode) OPAQUE SetBkMode GetBkMode
Цвет фона (Background color) Белый SetBkColor GetBkColor
Цвет текста (TextColor) Черный SetTextColor GetTextColor
Режим рисования (Drawing mode) R2_COPYPEN SetROP2 GetROP2
Режим растяжения (Stretching mode) SetStrethBltMode GetStrethBltMode
Режим закрашивания многоугольников (Polygon filling mode) ALTERNATE SetPolyFillMode GetPolyFillMode
Межсимвольный интервал (Intercharacter spacing) 0 SetTextCharacterExtra GetTextCharacterExtra
Начало координат кисти (Brush origin) (0,0) SetBrushOrgEx GetBrushOrgEx
Область отсечения (Clipping region) Not SelectObject, SelectClipRgn GetClipBox

Обычно данная структура содержит значения по умолчанию, определенные разработчиками. Вы имеете возможность менять указанные атрибуты по собственному желанию уже после того, как получили дескриптор контекста устройства. Но не надо забывать, что когда пользовательскому приложению требуется изменить какой-либо элемент структуры контекста устройства, оно должно производить эти действия исключительно через предназначенные для этого функции API, поскольку напрямую элементы структуры контекста устройства для изменения не доступны.
Помимо связывания функций вывода API с конкретным устройством, контекст устройства решает проблему чрезмерной нагрузки на систему при интенсивном вызове GDI-функций. Ведь если разобраться, то становится очевидным, что параметры в контексте устройства присутствуют для снижения нагрузки на подсистемы ядра от необходимости постоянной передачи одних и тех же атрибутов при каждом вызове GDI-функций.

Факт получения контекста устройства можно воспринимать в виде своего рода разрешения на рисование в собственной клиентской области. Как только вы получили контекст устройства, Вы можете начинать рисовать.

Контекст устройства ассоциирован с конкретным аппаратным устройством (видеоадаптер, принтер, память). Мы рассматриваем случай с видеоадаптером, в котором контекст устройства для пользовательского приложения обычно ассоциируется с определенным окном на экране (не со всем экраном!). Сопряжение логического изображения с определенным видеоадаптером осуществляется уже самой операционной системой на основе установленных в системе драйверов. Контекст устройства дополнительно выполняет еще и функцию абстракции (независимости) от конкретной аппаратной архитектуры, позволяющей приложению записывать данные в аппаратное устройство, абсолютно не заботясь о том, правильно или неправильно будет представлена информация на огромном количестве существующих или перспективных аппаратных устройств, имеющих свою специфику. Таким образом обеспечивается идентичность изображения на всех конфигурациях, на которых работает операционная система Windows.

Фактически дескриптор контекста устройства представляет собой число, которое является уникальным для каждого окна.

Контекст устройства можно представить еще в виде абстрактного окружения по умолчанию, которое Windows подготовила для вашего приложения.

Методы получения контекста устройства

Как уже упоминалось, для начала работы с контекстом устройства, требуется получить его описатель у операционной системы. Говоря простым языком, каждый раз, когда приложению надо что-то нарисовать, код при помощи специализированных функций должен запросить и получить дескриптор контекста устройства. А вот тут то начинается определенная конкретика. Дело в том, что методы запроса контекста устройства различаются в зависимости от условий и целей, при которых этот запрос выполняется:

Ситуация Метод получения
Обновление актуального содержимого окна по требованию системы в ответ на входящее сообщение WM_PAINT Использование контейнера из функций BeginPaint...EndPaint. Все рисование заключается внутри пары этих функций. В этом случае контекст устройства возвращается функцией BeginPaint; Между парой функций BeginPaint-Endpaint, вы можете вызывать любые GDI-функции для отрисовки в вашей клиентской области.
Отрисовка требуемого содержимого окна в ответ на входящее сообщение о создании окна WM_CREATE и остальные сообщения (кроме WM_PAINT) В данном случае мы не можем использовать контейнер из функций BeginPaint...EndPaint, поскольку обрабатываем не сообщение WM_PAINT. В данной ситуации мы можем использовать:

  1. Контейнер из функций GetDC...ReleaseDC;
  2. Контейнер из функций GetWindowDC...ReleaseDC;
В специфических (каких именно?) случаях. Использование контейнера из функций CreateDC...DeleteDC для создания своего собственного контекста устройства;

вот как-то примерно так:

Система координат

Опять же, если вспомнить систему 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 является системным сообщением и не рекомендуется к самостоятельной отправке кодом приложения в свою же очередь сообщений, надо поступать хитрее - использовать специальные функции. Например, для того, чтобы заставить систему сгенерировать сообщение WM_PAINT и одновременно задать область отрисовки, имеются функции InvalidateRect и InvalidateRgn. При использовании этих функций в очередь сообщений приложения будет отправлено сообщение WM_PAINT, обработка которого в нашем коде приведет к требуемому результату.

Пример 1: Вывод текста в окно с использованием функции DrawText

В этом примере мы будем производить вывод текста в наше окно при помощью функции DrawText в процедуре обработки сообщения WM_PAINT. В нашем приложении я решил выводить многострочный текст, поскольку слишком простые иллюстрации из категории Hello, World! несут в себе довольно мало практического смысла. Как мы помним, при рисовании в ответ на сообщение WM_PAINT, мы должны использовать следующий алгоритм:

  • Получить указатель (дескриптор) на контекст устройства при помощи функции BeginPaint;
  • Произвести рисование в клиентской области при помощи функции DrawText;
  • Освободить указатель (дескриптор) на контекст устройства при помощи функции EndPaint;

За основу для нашего примера был взят шаблон оконного приложения и выполнена его доработка. В том же материале была описана вся структура шаблона, поэтому здесь мы перейдем сразу к рассмотрению нового кода, выполняющего, непосредственно, вывод текста в окно. Давайте для начала приведем полный исходный код получившегося приложения, а затем начнем анализировать (изменения в исходном шаблоне выделены цветом):

Код обработчика сообщения WM_PAINT начинается у нас со строки 65. Перво-наперво в обработчике у нас вызывается функция BeginPaint, которая возвращает структуру pnt типа PAINTSTRUCT, содержащую координаты недействительной области. Эти координаты помогают выяснить, всю ли клиентскую область окна надо отрисовывать или только часть. Состав структуры pnt содержит информацию об отрисовке:

Описание параметров:

Наименование Определение
hdc Описатель (дескриптор) контекста устройства, в котором требуется перерисовка. То же самое значение, что и возвращаемое функцией в регистре eax.
fErase Необходимость затирания (очистки) фона окна в области, подлежащей обновлению.

  • TRUE - Windows сама перерисует фон, используя кисть hbrBackground, которая указывалась при регистрации класса окна в структуре WNDCLASS (WNDCLASSEX);
    При этом значении функция BeginPaint посылает процедуре окна сообщение WM_ERASEBKGND;
  • FALSE - фон обновляться не будет;
rcPaint Структура типа RECT, определяющая координаты недействительного прямоугольника, который требуется перерисовать. Координаты: left (x0), top (y0), right (x1), bottom (y1). Используя конкретный дескриптор контекста устройства (он же и в hdc), Вы не сможете рисовать вне этого недействительного прямоугольника. Значения координат недействительного прямоугольника даются относительно левого верхнего угла рабочей области окна.

Затем, в строках 68 и 69 у нас присутствует пара функций SetBkColor, SetTextColor, которые (соответственно) задают цвет фона и символов для текущего контекста устройства. В качестве цвета в GDI-функциях используется RBG-цвета размерностью 16млн. Напомню, что в данном конкретном примере для отрисовки текста у нас используется функция DrawText, которая подразумевает вывод текста в заданный прямоугольник с форматированием. Какой (извините за мой французский) прямоугольник? Зачем тут потребовался еще какой-то прямоугольник, когда вывод вроде бы должен и так производиться в прямоугольник, который представляет собой клиентскую области окна? Ничего не поделаешь, такая вот особенность функции DrawText, которая сперва выводит некий невидимый (виртуальный) ограничивающий прямоугольник, а только потом в нем рисует текст. Конечно эта внутренняя связка отрабатывает гораздо быстрее, чем это производилось бы в случае вызова двух функций. Соответственно, в коде нашего приложения, перед использованием функции DrawText, нам необходимо сформировать прямоугольник либо вручную, либо при помощи специализированной функции GetClientRect. Функция GetClientRect возвращает координаты клиентской области текущего окна, которые определяют верхний левый и нижний правый углы прямоугольника. После вызова, функция заполняет структуру rect типа RECT, представляющую из себя следующее:

Члены left (лево) и top (верх) инициализируются значением 0, а right (право) и bottom (низ) принимают значения ширины и высоты полной клиентской области окна, к которому применена функция. Думаю, что можно и обойтись без данной функции, но тогда нам каким-то образом (вручную или другой функцией) придется сформировать структуру rect, которая используется в функции DrawText, поскольку с не инициализированной структурой она попросту рисует "не туда".
Далее, в строке 71 расположена сама функция DrawText. Отмечу, что у данной функции есть ряд преимуществ перед иными функциями работы с текстом. Например, нет необходимости вычислять длину строку (как в случае с TextOut), достаточно всего-лишь указать значение параметра -1 и выведена будет вся строка до завершающего 0. Основное преимущество этой функции по отношению к TextOut, это возможность работать с дополнительными атрибутами, позволяющими нужным образом форматировать многострочные поля текста. Однако, это является одновременно и минусом, поскольку из-за дополнительного форматирования, функция DrawText медленнее своих аналогов. Помимо этого у функции имеется еще один небольшой недостаток, и состоит он как раз в необходимости предварительной инициализации ограничивающего прямоугольника (настройка которого иногда доставляет дополнительные неудобства). В нашем случае у функции задается метод форматирования текста DT_WORDBREAK, который подразумевает перенос не вмещающейся в прямоугольник строки в точке разрыва (пробела).
В результате работы программы у нас получается вот что:

Вывод текста DrawText

Советы по работе с функцией:

  1. По умолчанию текст, который не вмещается в прямоугольник, заданный структурой rect, обрезается по правому краю. Дабы этого не происходило, используйте константу DT_NOCLIP;
  2. Если Вам потребовалось вывести текст в несколько строк, или перенести длинную строку, используйте константу DT_WORDBREAK. Без неё текст, в любом случае, будет выводиться в одну строку, не смотря на наличие в строке управляющих символов (перевод строки(LF), возврат каретки(CR)). При использовании значения DT_CALCRECT и DT_WORDBREAK изменятся будет член bottom, без DT_WORDBREAK - right.
  3. Вертикальное выравнивание выводимого текста по центру с применением DT_VCENTER работает только для однострочного текста. Для полноценного центрирования многострочного текста необходимо писать алгоритм: вычислить размер 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;

В данном примере мы будем выводить в окно короткие текстовые строки разным шрифтом. Традиционно, за основу был взят исходный текст шаблона оконного приложения на ассемблере и слегка модифицирован. Изменения маркированы цветным выделением:

Новый код у нас начинается со строки 63, которая относится к обработчику сообщения WM_CREATE. Странно, причем тут сообщение WM_CREATE, если мы в рамках метода должны делать всё в обработчике WM_PAINT? Действительно, проще было бы сгруппировать все функции в обработчике WM_PAINT, но я решил немного оптимизировать код обработчика данного сообщения по производительности. Задайтесь вопросом, зачем нам каждый раз создавать/уничтожать объект шрифта в интенсивно используемом обработчике отрисовки, загружая процессор совершенно ненужными тактами и тормозя процесс? К тому же, модификация не нарушает принципа метода: вся отрисовка по-прежнему производится внутри обработчика сообщения WM_PAINT, просто некоторые подготовительные и заключительные действия вынесены (за скобки) в обработчики других сообщений: создание объекта шрифта в обработчик WM_CREATE, а удаление объекта созданного нами ранее шрифта в обработчик WM_DESTROY. Итак, в момент создания окна поступает сообщение WM_CREATE, и первое что мы делаем - вызываем функцию создания объекта шрифта (строка 63). Поскольку используемая нами функция CreateFont всем своим названием говорит о том, что вроде бы как она шрифт "создает", на самом же деле, функция видоизменяет существующий в системе шрифт на основе заданных входных параметров и создает объект полученного шрифта, который в последствии может быть использован в коде. На параметрах данной функции хотелось бы остановиться подробнее, поскольку на сайте разработчика не сильно заморачиваются пояснениями. Соответствие параметров: слева - направо.

Параметр Описание
nHeight Высота (символов) шрифта в логических единицах.
nWidth Ширина (символов) шрифта в логических единицах.
nEscapement Угол наклона опорной линии текста по отношению к горизонтальной оси (в десятых долях градуса). Для простоты понимания стоит отметить, что изменение параметра позволяет выводить строку текста под углом или вовсе вертикально.
nOrientation Угол наклона текста по отношению к отдельному символу (в десятых долях градуса). Параметр позволяет изменять наклон (поворачивать) самих символов в строке вне зависимости от наклона самой строки.
fnWeight Жирность (насыщенность) шрифта.
fdwItalic Курсив
fdwUnderline Подчеркнутость.
fdwStrikeOut Перечеркнутость.
fdwCharSet Кодировка.
fdwOutputPrecision Точность шрифта. Насколько точно создаваемый шрифт отвечает заданным параметрам: высоте, ширине, ориентации, шагу и типу шрифта.
fdwClipPrecision Точность прилегания шрифта. Метод отсечения части символов, не попадающие в видимую область.
fdwQuality Качество шрифта. Соответствие логического и физического шрифтов. Параметр определяет, насколько тщательно GDI должен пытаться соблюдать соответствие между атрибутами логического шрифта и соответствующими атрибутами физического.
fdwPitchAndFamily Семейство шрифта.
lpszFace Название шрифта. Указатель на строку символов, содержащую название семейства шрифтов.

Семейство шрифтов можно выбирать несколькими способами: через задание константы входного параметра fdwPitchAndFamily, либо через указание параметра lpszFace. В строке 65 выполняется контроль выполнения функции, проверяется выходное значение в регистре eax, и если оно нулевое (0, NULL), что означает ошибку, то происходит выход из оконной процедуры с выставлением значения на выходе равного -1.
Обработчик сообщения WM_PAINT начинается со строки 73. Перво-наперво вызывается у нас функция BeginPaint для получения координат недействительной области (в структуре pnt) и сохранения дескриптора контекста устройства (в переменной pHMain, строка 74). Затем у нас в коде следует неизвестной нам ренее функции SelectObject, которая производит выбор объекта нового шрифта.

Функция SelectObject назначает GDI-объект, дескриптор которого указан в качестве параметра, текущим (активным) объектом определенного типа в указанном контексте устройства. Иными словами, функция позволяет (присоединить) выбрать GDI-объект в контексте устройства.

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

Зачем такая сложность с переключением объектов? By Design! Как вы уже, вероятно, успели заметить, функции по работе с текстом не имеют входных параметров, определяющих шрифт, размер букв, цвет фона, цвет букв и прочих настроек стиля отображения. Подобные атрибуты текста хранятся в соответствующих членах структуры контекста устройства и для переключения этих характеристик как раз и предназначена функции создания и переключения объектов контекста.

Другими словами, рекомендовано восстанавливать контекст устройства в состояние, в котором он находился до переключения в вашем коде. Если вы меняете какие-либо из GDI-объектов, вы должны восстановить значения на оригинальные до возврата из обработчика. Возвращаемое функцией SelectObject значение является: если выбранный объект не регион - дескриптор замененного объекта, если если выбранный объект является регионом - возвращаемое значение определяется следующей таблицей. Фактически, функция SelectObject возвращает описатель заменённого (предыдущего) объекта того же типа для конкретного контекста устройства.

Вычисление длины строки

Однако функция TextOut имеет один небольшой недостаток: ей необходимо задавать длину выводимой строки. На просторах Сети я нашел пару приемов по вычислению длины строки.
метод статический:

метод динамический:

Второй метод хорош тем, что вычисление длины происходят в момент выполнения кода. Плох тем, что размер кода увеличивается, появляется необходимость в вызове дополнительных функций, как у DrawText! В нашем примере, в строке 77 кода, используется второй из описанных методов. В строке 79 у нас повторно используется функция SelectObject для восстановления значения объекта шрифта контекста устройства. После вызова завершающей функции EndPaint, код обработчик сообщения WM_PAINT фактически заканчивается.
В результате работы нашего приложения, на экран выводится окно со следующим содержимым:

Вывод текста TextOut

В обработчике сообщения WM_DESTROY (строка 84), непосредственно перед закрытием основного окна программы и выходом из неё, мы выполняем удаление созданного нами объекта нового шрифта. Опять же, по сравнению с эталонным шаблоном, в данном примере (строка 113) у нас подключается новая библиотека gdi32.dll.

Метод 2: рисование в обработчиках других сообщений

Раздел в стадии разработки!!

Как у разработчика под Windows достаточно "зеленого", у меня отсутствует опыт программирования всех возможных вариантов вывода текста в окно на ассемблере. Однако, догадываюсь, что в практике программиста встречаются ситуации, когда требуется изменить содержимое окна не в обработчике сообщения WM_PAINT, а в каком-либо другом месте приложения. Другими словами, в реальных приложениях вывод текста в окно может производиться в различных обработчиках событий, в произвольном месте кода. Стоит ли так делать, это вопрос уже второй, но как поступать в случае необходимости? Я думаю, в этом случае у нас имеется единственный путь решения задачи:

  • Код в обработчике (в котором нужно произвести отрисовку) должен использовать контейнер из пары функций GetDC...ReleaseDC либо GetWindowDC...ReleaseDC, в который помещаются все необходимые функции отрисовки. В дополнение, необходимо использовать функцию ValidateRect, которая позволяет сделать любую прямоугольную область окна действительной, иначе в очередь сообщений система постоянно будет спамить сообщение WM_PAINT.

Я думаю надо будет попробовать запрограммировать этот способ. Положительный момент данного подхода состоит в том, что он позволяет рисовать во всей рабочей области окна, а не только в недействительной области. Небольшое неудобство заключается в самостоятельной ратификации недействительной области, при помощи специализированных функций, потому как ReleaseDC этого не делает.

Метод 3: Создание собственного контекста

Раздел в стадии разработки!!

При использовании данного способа требуется валидировать недействительную область самостоятельно, при помощи спец. функций, потому как ReleaseDC этого не делает!

Правила (он же не разобранный мусор)

Итак, давайте ка сведем все требования в одно место:

  1. Для начала отображения вы должны получить контекст устройства при помощи функции, зависящей от способа реакции.
  2. Непосредственно перед выполнением действий по выводу информации в клиентскую область, приложение должно определить её размерность.
  3. Приложение должно обеспечивать перерисовку внутренней поверхности (или части) любого из собственных окон в любой момент времени при получении сообщения WM_PAINT.
  4. Если Вы в своём приложении не обрабатываете сообщение WM_PAINT, требуется по крайнем мере вызвать функцию DefWindowProc либо ValidateRect для подтверждения недействительной области, в противном случае Windows будет непрерывно слать сообщения WM_PAINT вашему окну.
  5. После того, как Вы закончили работу с дескриптором контекста устройства, Вы должны освободить его до завершения кода обработки сообщения, то есть до окончания обработки сообщения. Не рекомендуется получать дескриптор в ответ на одно сообщения, а освобождать в ответ на другое!
  6. При использовании контейнера функций BeginPaint/EndPaint, система автоматически ограничивает рисование вне области обновления, другими словами вы не сможете что-нибудь нарисовать вне области обновления. Поэтому при использовании BeginPaint/EndPaint стоит это помнить. Информация, нарисованная на действительной области, просто не будет отображена на экране.
  7. В параметрах функций API Windows, отвечающих за обработку текста, нет стандартной поддержки выравнивания многострочного текста по ширине.
  8. В процессе перерисовки генерируется еще сообщение WM_ERASEBKGND (когда именно?). Как правило его не обрабатывают, поэтому оно передается функции DefWindowProc, которая в ответ перерисовывает фон соответствующей (недействительной?) области окна (используя кисть, определенную при регистрации класса окна). Последовательность со стороны системы тут следующая: перед записью в очередь сообщений приложения сообщения WM_PAINT, система посылает процедуре окна сообщение WM_ERASEBKGND. Если процедура окна не имеет обработчика сообщения WM_ERASEBKGND, попросту передавая его умолчальной функции DefWindowProc, последняя в ответ на данное сообщение закрашивает внутреннюю область окна с использованием кисти, указанной в соответствующем классе окна (задается при регистрации класса окна). Из этого следует, что если оконная процедура нарисует что-либо в окне во время обработки других сообщений (отличных от WM_PAINT), после прихода первого же сообщения WM_PAINT нарисованное изображение будет закрашено.
  9. Индивидуальные вызовы различных функций (DrawText, TextOut, PolyTextOut и прочих) не вызывают сиюминутного изменения в клиентской области. Вместо этого ядро системы буферизирует (накапливает) запросы отрисовки, и только по вызову функции EndPaint или другой, освобождающей контекст, система выполняет обновление клиентской части окна.
  10. Функции по работе с текстом не имеют входных параметров, определяющих шрифт, размер букв, цвет фона, цвет букв и прочих настроек стиля отображения. Подобные атрибуты текста хранятся в соответствующих членах структуры контекста устройства. Поэтому, для переключения этих характеристик необходимо использовать специальные функции создания и переключения объектов контекста.
  • Поделиться:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *