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

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

Вывод текста в окно, как способ визуализации разнообразного рода данных, является одной из основных задач в любом языке программирования. На пути изучения программирования на языке Ассемблера под Windows, очередным шагом является освоение принципов работы со строковой информацией в рамках использования объектов графического интерфейса (окон). В последние десятилетия человечество заметно изменило механизмы взаимодействия пользователя с операционной системой в сторону использования графики, постепенно нивелируя роль текстовых режимов работы, объясняя это удобством использования. Тем не менее, в области применения графических интерфейсов, мы так и не отошли от основной формы выражения языка - письменности, потому как текст был и остается одним из превалирующих типов передачи информации. Не смотря на тотальное засилие графики во основных пользовательских операционных системах (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) - регион клиентской части окна приложения, содержимое которого потеряло актуальность и требует обновления (перерисовки).
  • Действительная (не требующая перерисовки) область (valid rectangle) - регион клиентской области окна приложения, графическая информация о котором известна операционной системе.
  • Обновляемый регион (update region) - регион, описывающий все недействительные области определенного окна, требующие перерисовки.

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

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

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

  • Данные о всех состояниях перерисовываемого содержимого окна должны храниться в пользовательском приложении. Это существенно меняет подход к написанию приложений, однако это не такая уж и сверхзадача, поскольку реализовать хранение состояний выводимых в окно данных в переменных/буферах приложения не так уж и сложно.
  • Для упрощения алгоритма перерисовки содержимого (части) окна, весь вывод (информации в окно) настоятельно рекомендуется производить одном месте кода - в обработчике сообщения 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) BLACKONWHITE 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

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

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

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

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

По окончании рисования контекст устройства необходимо освободить.

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

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

Ситуация Метод получения
Обновление актуального содержимого окна по требованию системы в ответ на входящее сообщение WM_PAINT Рекомендовано использование контейнера из функций BeginPaint...EndPaint. Все рисование заключается внутри пары этих функций, между которыми вы можете вызывать любые GDI-функции для отрисовки в клиентской области. В этом случае контекст устройства возвращается функцией BeginPaint;
Отрисовка требуемого содержимого окна в ответ на входящее сообщение о создании окна 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. При использовании этих функций обновляемый регион (update region) становится непустым, и система, в ответ на это, отправляет в очередь сообщений приложения сообщение WM_PAINT, обработка которого в нашем коде приведет к требуемому результату. В некоторых случаях для достижения аналогичного результата может применяться связка функций InvalidateRect / InvalidateRgn + UpdateWindow, в некоторых функция RedrawWindow.

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

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

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

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

Хотелось бы обратить ваше внимание на стили класса окон, задаваемые в строке 16. Члену style структуры wc класса мы присваиваем комбинированное значение констант CS_HREDRAW и CS_VREDRAW (объединяя их операцией ИЛИ). Эти значения предписывают произвести перерисовку всего окна (созданного на основе данного класса) при его перемещении либо изменении ширины и высоты в ответ на изменение размера. При экспериментах без использования данных констант у меня часто возникали ситуации, в которых при обновлении недействительных областей окна отрисовывалась только часть окна, уродуя строки текста.

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

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

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

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

Как мы видим, большой блок текста прекрасно "вписался" в клиентскую область окна.
Некоторые советы по работе с функцией DrawText:

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

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

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

Приложения работают с так называемыми логическими (создаваемыми на основе заданных параметров), а не с физическими (сконфигурированными в системе) шрифтами. Логический шрифт представляет собой GDI-объект, созданный в контексте устройства вывода на основе заданных параметров, определяющих требования к шрифту со стороны приложения.

Как следует из названия используемой нами функции 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, которая предназначается для выбора объекта нового шрифта.

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

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

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

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

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

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

.. а второй способ вот таким:

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

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

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

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

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

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

Основной плюс данного метода заключается в том, что чуть легче настроить рисование во всей рабочей области окна, а не только в недействительной его части, а минус заключается в необходимости самостоятельной валидации недействительной области, при помощи специализированных функций, потому как ReleaseDC этого не делает.
В случае, когда приложение у нас построено так, что частичное (мгновенное) рисование осуществляется в произвольных обработчиках, а основное в обработчике WM_PAINT, в дополнение (зачастую) в коде произвольного обработчика включают функцию InvalidateRect либо InvalidateRgn, которые позволяют объявить любую прямоугольную область окна недействительной, с целью генерации сообщения WM_PAINT и последующего вызова её обработчика.
Скажу честно, в качестве иллюстрации к данному методу приложение у меня получилось избыточно-кривое и явно надуманное, однако суть оно позволяет уловить. Алгоритм его достаточно примитивен, и заключается в том, что в момент нажатия левой кнопки мыши в собственном окне, рисует символ в позиции курсора мыши. Давайте, сперва, более детально распишем алгоритм работы примера:

  • В обработчике сообщения WM_LBUTTONDOWN: Получить координаты курсора мыши, сохранить их в локальные переменные;
  • В обработчике сообщения WM_LBUTTONDOWN: Получить контекст устройства при помощи функции GetDC;
  • В обработчике сообщения WM_LBUTTONDOWN: Произвести отрисовку символа при помощи функции TextOut;
  • В обработчике сообщения WM_LBUTTONDOWN: Освободить контекст устройства при помощи функции ReleaseDC;
  • В обработчике сообщения WM_LBUTTONDOWN: Объявить всю клиентскую область окна недействительной при помощи функции InvalidateRect. Таким извращенным образом у нас реализовано стирание символа в старой позиции :);
  • В обработчике сообщения WM_PAINT: Получить указатель (дескриптор) на контекст устройства при помощи функции BeginPaint;
  • В обработчике сообщения WM_PAINT: Произвести отрисовку символа при помощи функции TextOut;
  • В обработчике сообщения WM_PAINT: Освободить указатель (дескриптор) на контекст устройства при помощи функции EndPaint;

а теперь приведем код приложения вывода текста в окно в обработчике WM_LBUTTONDOWN:

Ну, оценили всю извращенность приведенного кода? :) Как видно из листинга, исходный код, мягко говоря "своеобразный", потому как я со своим уровнем понимания предметной области не смог придумать ничего лучше :) Приложение реализовано таким образом, что отрисовка (вывод) символа осуществляется мгновенно обработчике сообщения WM_LBUTTONDOWN, то есть непосредственно при нажатии клавиши мыши. В данном обработчике мы получаем из параметров сообщения координаты курсора мыши и сохраняем их во внутренние переменные mousepos_x, mousepos_y, по которым затем функция TextOut производит отрисовку одиночного символа. Еще одна отрисовка у нас, традиционно, выполняется в обработчике сообщения WM_PAINT, поскольку без её реализации не будет обновляться содержимое окна при операциях перемещения и изменения размера. Очевидно, что при хорошей оптимизации кода примера, можно было бы реализовать всю логику рисования в обработчике WM_PAINT, а в обработчике WM_LBUTTONDOWN просто работать с координатами курсора. К тому же, присутствует явно "вынужденная" функция InvalidateRect, которая применена тут для упрощения очистки фона окна, который выполняется для затирания символа в старой позиции.

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

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

Используются функции CreateDC, CreateCompatibleDC, DeleteDC. При использовании данного способа, так же, требуется валидировать недействительную область самостоятельно.

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

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

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

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

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