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

Метки:  , , , , , , , , , , ,
Все нижеизложенное в значительной степени применимо ко всем видам графических объектов (а не только текстовой (строковой) информации).

Вывод текста в окно, как способ визуализации разнообразного рода информации, является одной из основных задач в любом языке программирования. На пути изучения программирования на языке Ассемблера под Windows, очередным увесистым камнем преткновения является освоение принципов работы со строковыми данными (текстом) в рамках использования объектов графического интерфейса (окон). В последние десятилетия человечество заметно изменило механизмы взаимодействия пользователя с операционной системой в сторону использования графики, постепенно нивелируя роль текстовых режимов работы, объясняя это удобством использования. Текстовый режим в виде практически не потерявшей свой первоначальный вид консоли, ведущей своё начало еще от телетайпов и пишущих машинок, всё еще присутствует в составе перечисленных ОС. И даже в области применения современных графических интерфейсов, мы так и не отошли от основной формы выражения языка - письменности, потому как текст был и остается одним из превалирующих типов интерпретации информации. Если говорить о ранних этапах становления, то предшественницей системы Windows была MSDOS, в которой интерфейс командной строки функционировал в текстовом режиме. Помните, как в ней реализовывался вывод текста на ассемблере, который осуществлялся через функции прерывания 10h BIOS, либо попросту прямой записью данных в область видеопамяти (сегмент 0B800h).

С одной стороны всё было довольно просто и увлекательно, ведь экран можно было представить в виде телетайпа, а вывод отождествлялся с потоком символов, автоматически "заворачивающемся" (переносящемся) при окончании очередной строки. Видеопамять отображалась на физический дисплей видеоадаптера таким образом, что можно было не заботиться о переносе строк как посредством прямого вывода в видеопамять, так и через функции прерывания 10h. Текстовый режим представлял собой матрицу ячеек фиксированного размера (8x8, 8x14, 8x16, ...), что существенно упрощало создание объектов любой сложности, начиная от одиночных символов и заканчивая полнофункциональными оконными интерфейсами. С другой стороны, во времена низкоуровневого программирования вывода под MSDOS, существовали и проблемы:

  • специфическая настройка под разные типы адаптера (CGA, VGA, SVGA): режимы, параметры вывода и прочее;
  • низкоуровневые особенности отдельных видеокарт: порты, методы задания параметров отображения, наборы команд и прочее.
  • бесконтрольный доступ к оборудованию: "в экран" мог беспрепятственно писать любой код, выполнявшийся в системе в данный момент времени.

Что же касается графики, то слишком много аспектов приходилось принимать во внимание при написании графических библиотек, учитывавших особенности распространенных видеоадаптеров. Да, мир тогда был совершенно иным и мы попросту к нему привыкли. А потом наступил 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 далеко не такая тривиальная задача, как её аналог для текстового режима в MSDOS, и тут нам необходимо усвоить ряд правил:

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

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

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

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

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

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

window client area

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

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

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

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

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

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

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

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

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

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

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

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

Контекст устройства [отображения]

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

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

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

Ситуация Метод получения
Обновление актуального содержимого окна по требованию системы в ответ на входящее сообщение WM_PAINT Рекомендовано использование контейнера из функций BeginPaint...EndPaint. Все рисование заключается внутри пары этих функций, между которыми вы можете вызывать любые GDI-функции для отрисовки в клиентской области. В этом случае контекст устройства возвращается функцией BeginPaint;
  • Прорисовка требуемого содержимого окна в ответ на любые другие (кроме WM_PAINT) входящие сообщения (например о создании окна WM_CREATE и прочие);
  • Прорисовка требуемого содержимого окна вне процедуры обработки сообщений окна, внутри очереди сообщений;
В данном случае мы не можем использовать контейнер из функций 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 имеет один небольшой недостаток: в одном из входных параметров необходимо задавать длину выводимой строки. Для вычисления длины строки используется несколько подходов, которые подробно описаны в этой вот статье. В нашем примере (в строке 78) для вычисления используется системная функция lstrlen, возвращающая длину строки в регистре eax (без учета нуль-терминатора), а непосредственно за ней вызывается функция TextOut, выводящая строку в окно. В строке 80 у нас повторно используется функция SelectObject для восстановления значения объекта шрифта контекста устройства. После вызова завершающей функции EndPaint, код обработчик сообщения WM_PAINT фактически заканчивается.
Результатом работы нашего приложения является вывод на экран окна со следующим содержимым:

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

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

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

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

Функция GetDC используется для выполнения отрисовки, которая должна производиться мгновенно, без привязки к обработке сообщения WM_PAINT.

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

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

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

  • В обработчике сообщения потока: получить контекст устройства при помощи функции GetDC;
  • В обработчике сообщения потока: по заданным координатам в окне произвести отрисовку символа при помощи функции TextOut;
  • В обработчике сообщения потока: перед выходом из программы освободить указатель (дескриптор) на контекст устройства при помощи функции ReleaseDC;

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

Как можно заметить после изучения кода, логика у нас действительно существенно отличается от предыдущих примеров данной статьи: вывод объектов в окно происходит без остановки, постоянно, как говорится, без какого-либо пользовательского участия. Кстати, по похожей схеме функционируют разнообразные игры и демки. После создания окна, при помощи функции GetDC (строка 32) мы запрашиваем описатель контекста устройства (DC) для клиентской области заданного окна, при этом сохраняя его в переменную. Затем, уже в самом обработчике сообщений потока у нас используется новая пока для нас функция PeekMessage (строка 40), которая проверяет очередь потока без ожидания (поступления сообщения), и в случае отсутствия оного (ведь обычно мы ничего не делаем с коном, просто смотрим демку) передает управление на метку no_message, где как раз и происходит вывод строки при помощи функции TextOut (строка 51). Таким вот нехитрым алгоритмом создается непрерывный вывод в окно, думаю Вам будет полезна эта реализация при создании собственных демок/игр. При попытке закрыть окно по комбинации Alt-F4, производится освобождение дескриптора контекста устройства при помощи функции ReleaseDC (строка 62).

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

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

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