Crippled inside
АрхивХакеры думают, что в аду их ждет Doom 8.
Но они ошибаются. Их там ждет Windows 2000.
(Из присланного Антонио Эпада)
Я принимаю участие в создании информационной системы, и одной из моих задач является разработка подсистемы выпуска документов: карт, отчетов и т. д. Естественно, что, когда выводишь карту (да и любой другой документ), неплохо бы иметь подписи и условные знаки именно там, где они были поставлены во время редактирования, и желательно, чтобы они имели именно тот размер, который им был задан. И вот с чем я столкнулся при попытке реализовать пресловутый WYSIWYG под Windows. Вроде бы все прекрасно: аппаратно-независимая графическая библиотека, различные режимы отображения, позволяющие программисту не заботиться о том, какая разрешающая способность у реального устройства. В общем-то, так оно и есть.
Проблемы начинаются при выводе текстов. Если вас не беспокоит то, что строка текста на экране выглядит раза в полтора длиннее, чем на принтере, то вам вполне хватит базовых функций API (Application Program Interface). Но если вы хотите получить WYSIWYG… то вы его не получите! В справочной системе Windows API так и написано: "достижение истинного WYSIWYG невозможно и даже нежелательно во многих случаях". Насчет нежелательности можно поспорить, а вот в невозможности сомневаться не приходится. Объясняется это двумя вещами: тем, что базовые графические функции целочисленные, и слишком большой разницей в разрешающих способностях принтера и дисплея. Координаты и размеры при рисовании задаются логическими единицами, по умолчанию равными пикселу устройства. Тут, конечно, никакого WYSIWYG не выйдет. Линия из 100 пикселов на экране (96 dpi) имеет длину около 2,5 см, а на принтере (600 dpi) та же самая линия из 100 пикселов уложится всего в 4 мм. Но можно попросить логическую единицу в одну десятую и даже в одну сотую миллиметра. Вроде бы все хорошо. При рисовании на экране заказываю 0,1 мм, при печати на принтере заказываю тоже 0,1 мм - и вот вам вожделенный WYSIWYG.
С рисованием линий, прямоугольников и эллипсов все прекрасно. Но не со шрифтами! Строка текста на экране упорно занимает гораздо больше места, чем на листе бумаги. Дело в том, что при выводе строки координаты каждой следующей буквы вычисляются следующим образом: к координате последней выведенной буквы прибавляется ее ширина. И координаты, и размеры букв целочисленные. Допустим, заказал я шрифт высотой 20 пикселов. Тогда ширина буквы А будет равна 13,6 пиксела. Но пиксел-то штука неделимая, поэтому для буквы А генерируется битмап размерами 20x14 пикселов. При выводе эти 0,4 пиксела не учитываются, поэтому буквы в строке имеют тенденцию сползать в сторону. Ну и ладно, что такое один пиксел на букву? Но если для лазерного принтера 1 пиксел - это 0,04 мм, то для экрана это уже 0,2 мм. Разницу ощущаете? К тому же эта ошибка к концу строки накапливается. Обойти это можно одним-единственным способом: вычислять координаты букв в формате float (ошибка округления для каждой буквы останется, но для строки в целом ее не будет). Windows этого не делает.
Ладно, я согласен вычислять координаты сам и выводить каждую буковку отдельно. Для этого мне нужно знать ширину каждого символа. Вот тут начинается самое интересное. В Windows API имеется целый букет функций, выдающих ширины символов. Почти все они целочисленные, то есть выдают уже округленные значения. Есть функция GetChatWidthFloat, выдающая ширину символа в формате float. В каких единицах эта ширина измеряется, мне выяснить не удалось. В help'е даже намека на это нет. Немного поэкспериментировав, я выяснил, что если умножить это число на 16, то получаем целое (!) число, в точности равное тому, что выдает целочисленная функция. Зачем это может пригодиться, я так и не придумал. Есть также функция GetCharABCWidthsFloat(), которая в режиме отображения MM_TEXT (одна логическая единица равна одному пикселу) опять-таки выдает целые значения. Дробные части всегда равны нулю. В общем, как ни верти и как ни тычь, а все эти функции выдают ширину битмапа в пикселах, пересчитанную для конкретного режима отображения. То есть отвязаться от конкретного устройства не получается.
Если вы собираетесь пользоваться только каким-нибудь одним принтером, то Microsoft предлагает вам вот что: поскольку разрешение принтера значительно выше экранного, то создавайте принтерный контекст, спрашивайте у него размеры букв и, пользуясь этими принтерными размерами, выводите буквы на экран. Это они называют принтер-ориентированным WYSIWYG'ом. Но если вы принтер смените (или поменяете у него разрешение), то вся ваша верстка полетит коту под хвост, что, кстати, в прежних версиях WinWord одно время и случалось (в Office 97 они это, похоже, исправили). Для полной отвязки от устройства нужно всего-навсего иметь возможность получать размерности символа в единицах, в которых он был разработан (design units). В процессе разработки все символы рисуются в своей координатной сетке, равной обычно 2048x2048 (так называемый em square). Но так как функции API выдают мне размеры битмапов, то точность этих величин очень сильно зависит от разрешения устройства. Уменьшить эту зависимость можно, заказывая символ очень большого размера. Такого, чтобы размер пиксела был во много раз меньше размера буквы. Погрешность таким образом можно уменьшить, и отношение высоты к ширине будет практически одинаковым для различных устройств. Microsoft рекомендует заказывать шрифт высотой, равной em square, спрашивать у него ширину и другие размерности символов, вычислять нужные коэффициенты и затем пользоваться ими при выводе символов нормального размера. Таким способом можно получить размерности символа с точностью, с которой данный шрифт разработан. Этого должно хватить для любого принтера и экрана.
Не знаю, как вам, а мне такой подход не нравится. К чему такие изощрения? Всего-то и надо было: пользоваться математическим сопроцессором. Не пользуются. Это можно понять: хотят, чтобы Windows работала даже на самых дохлых машинах. Ну дайте мне тогда возможность получать размерности символа, хоть и целочисленные, но в его внутренних единицах (design units), а не в единицах устройства (device units). Не дают. Все функции хотят в качестве параметра контекст; выходные данные, соответственно, зависят от разрешения конкретного устройства. Приходится по всякому изгаляться. Я не хочу сделать из Windows издательскую систему (хотя почему бы и нет?), но если вы говорите, что поддерживаете технологию TrueType, так сделайте эту поддержку полноценной. А то существующей виндузовой реализации хватает только на подпись окошек и кнопочек, да еще Web-страничку можно поприличнее оформить.
Вот вам еще одна страшная история. Году этак в 1994-м я увидел прозрачное меню в игре "Comanche", и с тех пор меня не покидало желание сделать такое же. Так, чтобы было видно, что там под ним находится, заставить окно отбрасывать мягкую тень (как это сделано в пакете KAI Power Tools). В Windows для подобных извращений предусмотрен механизм переопределения (subclassing) функции, реагирующей на сообщения и отвечающей за прорисовку окна том числе. На этом была построена библиотека CTL3D, с помощью которой можно было придать более или менее приличный вид стандартному интерфейсу Windows 3.1. Но при ближайшем рассмотрении оказалось, что меню в Windows относятся к ресурсам, а не к элементам управления, и необходимого для перехвата идентификатора окна HWND не имеют. То есть они его, наверняка, имеют, но потрогать не дают. Возможность рисовать элементы меню самому (owner draw) предусмотрена, но мне нужно рисовать все меню полностью, а не поэлементно. Таким образом, обойтись написанием только функций прорисовки, а все остальное предоставить делать Windows не удалось. Пришлось писать меню полностью заново.
Для реализации прозрачности нужен alpha-канал. Для Windows это - пустой звук, она про такую штуку, как прозрачность, понятия не имеет. Структура RGBA имеется, но A-составляющая никак не используется. Отображение битмапов производится функцией BitBlt. Операции типа AND, OR и XOR она способна выполнять, а смешение (blend) и умножение (multiply), которые мне потребовались, она делать не умеет. Это я знал заранее и поэтому сразу приготовился писать свои функции. Но тут оказалось, что Windows тщательнейшим образом скрывает область памяти, в которой расположен собственно битмап. Имеется функция, выдающая массив байт, описывающих картинку. Но в тару заказчика. Выделяете дополнительную память, и Windows может вам туда скопировать битмап. Есть и обратная функция. Но мне не нужно копировать! Я достаточно сообразительный и хочу покопаться прямо и непосредственно в битмапе! Если я для каждой операции буду просить ее копировать туда-сюда, то никакого быстродействия не хватит! Да, еще забыл сказать, мне по ходу дела пришла идея показывать прозрачные меню не сразу, а постепенно, примерно за 0,2 секунды, проявлять их. Для особых, видимо, извращенцев в Windows все-таки имеется функция, дающая доступ непосредственно к битмапу, но только если вы его из массива байтов и создали. Функции же, читающие битмап из файла или из ресурсов, выдают его в виде типа HBITMAP, прямого доступа к которому Windows не дает. Пришлось в ресурсах размещать заготовки для мягкой тени (предварительно нарисованные в Photoshop'е) не в виде типа bitmap, а в виде бестипового массива двоичных данных и потом самостоятельно конвертировать их в вид, понятный Windows. Сначала я использовал для прорисовки только функции из Windows API. Одну картинку (один кадрик) прозрачного меню размерами где-то 150x200 точек Windows рисовала за 93 миллисекунды. Ни о каком, задуманном мною плавном проявлении, естественно, речи быть не могло. Зато после того, как я написал свои функции манипулирования битмапами (Windows я привлекал только для грабежа фоновой картинки с экрана и для вывода обратно на экран готового кадра), время прорисовки уменьшилось до 16 миллисекунд. Уже можно втиснуть десяток кадров! Вот вам пример того, как ненужными действиями можно свалить с ног любой процессор. Операционная система все-таки должна выполнять базовые функции быстрее (по крайней мере не медленнее) приложения, а не отнимать у него драгоценное время на собственные нужды. Если я могу не особо напрягаясь написать функции, работающие в 5 раз быстрее стандартных системных, то на кой черт мне такая система нужна?
А ведь делают же люди нормальные API. Возьмите, например, BeOS.
Во-первых, весь API полностью объектный. Исключение составляют некоторые сетевые функции и функции ядра. Ничего общего с монстром типа MFC, все "мило и просто". Полное описание в формате HTML занимает 3 мегабайта, в то время как справочная система Win32 API вместе с MFC приближается к сотне мегабайт упакованного текста!
Во-вторых, графическая библиотека использует для хранения размерностей и координат формат float (размеры битмапов тоже во float). Единица измерения - пункт. 1/72 дюйма. Я было подумал, что тут они маху дали - ведь не всегда удобно в пунктах работать. Для рисования всяких окошек, кнопочек и т. д. лучше к пикселам адресоваться. Но они вывернулись очень просто - приняли разрешение экрана всегда равным 72 dpi, независимо от действительного разрешения. Таким образом, для экрана 1 пункт всегда равен одному пикселу. Начало координат контекста (класс BView) можно двигать как угодно. Можно также менять коэффициент увеличения/уменьшения. Контекст может быть "присоединен" к окну, принтеру или битмапу.
В-третьих, не надо городить монструозный switch, разбирающий, что за сообщение пришло окну. Существует класс BWindow, у которого имеются методы, реагирующие на соответствующие сообщения. Наследуете этот класс - и переопределяете, что хотите. Все сообщения, кстати, передаются через класс BMessage, в который можно напихивать произвольное число параметров любых типов и в любом порядке. И не мучиться разборками двух DWORD'ов, как это сделано в Windows. Любой класс может сам себя "упаковать" в BMessage, после чего это сообщение можно послать кому-нибудь. А получатель может распаковать класс и использовать по назначению.
И вот как можно было бы сделать мои меню в BeOS. Возможности попробовать живую систему мне пока не представилось, так что все дальнейшие рассуждения носят чисто теоретический характер. Во-первых, в BeOS'овском API имеются классы BMenu и BMenuItem, поэтому переопределяем только методы рисования, а переписывать все меню заново не надо. Во-вторых, BeOS прекрасно осведомлена о существовании alpha-канала и о прозрачности. Имеются операции OVER (вывод с учетом alpha-канала), BLEND (среднее арифметическое), ADD и SUBTRACT. Хотите реализовать недостающие операции? Пожалуйста! Битмапы хранятся в классе BBitmap, у которого есть функция Bits(), возвращающая адрес массива байт, описывающих картинку. Копайтесь сколько хотите. Хотя свистопляска с созданием-удалением контекстов и осталась, но в значительно меньшей степени. В Windows, чтобы получить простую копию битмапа (и вообще для любых манипуляций с битмапами), вам нужно создать два контекста, выбрать в них битмапы, произвести нужную операцию, после чего восстановить эти контексты (так, чтобы ваши битмапы оказались невыбранными) и, наконец, удалить оба контекста. В BeOS все делается несколько иначе. Для прорисовки картинки вы просто вызываете у класса BView метод DrawBitmap() и передаете ему указатель на объект BBitmap. Весь фокус в том, что объект BView может быть "присоединен" как к окну и принтеру (BWindow и BPrintJob), так и к битмапу. Никаких карандашей и кистей, никаких "выборов" в контекст. Зачем в Windows это придумали, мне совершенно непонятно. Сплошные неудобства. Карандаш создай, кисть создай, шрифт создай, выбери в контекст, нарисуй чего надо, потом позаботься о том, чтобы контекст восстановить в первоначальной форме. Карандаши, кисти и шрифты удали (не дай бог карандашик куда закатится, в Win31 системные ресурсы вылетали после пяти минут работы), причем если какой-то из графических объектов на момент удаления был выбран в контекст, то такой объект не удаляется, а продолжает занимать память. Свихнуться можно! К тому же все эти выборы-перевыборы время занимают, хоть и немного, но все же. В BeOS ничего этого нет. Ее графическая библиотека напоминает борландовскую, BGI'ишную. Просто говорите, что сейчас цвет карандаша такой-то, ширина такая-то, цвет кисти такой-то. То же самое со шрифтами и битмапами. Все параметры хранятся в самом контексте. Когда угодно любой из них меняете. Если вам вдруг приспичило писать наклонным шрифтом, то не надо для этого создавать новый шрифт. Просто говорите, что далее пишем наклонным. Не надо ничего создавать, выбирать, сохранять и помнить.
В конце концов, из всего обширного и могучего Windows API я использовал всего две функции - DrawText и BitBlt, причем последнюю я использовал исключительно для общения с экраном, все остальное мне пришлось писать самому. В BeOS же мне предлагают использовать готовое меню и в битмапах дают копаться безо всяких оговорок, и операции с прозрачностью для меня делают.
MFC и OWL я игнорирую. Это всего-навсего надстройки, они лишь "классифицируют" существующие функции, пытаются скрыть врожденные недостатки виндузового API. А кое-какие еще и прибавляют. Один размер скомпилированного EXE-файла чего стоит (о времени компиляции я молчу), не говоря уже о куче необходимых DLL'ей. Что это за программа, если простое перемещение курсора мыши над окном занимает иногда до 80% процессорного времени, когда вся эта толпа универсальных классов начинает мусолить сообщение WM_MOUSEMOVE?
Посмотрите на регистрационную карточку Microsoft Visual C++. На ней написано: "Зарегистрируйтесь сейчас и получите поддержку, в которой нуждаетесь". И рядом фотография мужика с двумя костылями в руках. Очень мило. Гейтс, по всей видимости, считает программистов калеками и предлагает им костыли в образе MFC, чтоб они хоть как-то могли шевелиться. А может, он службу технической поддержки имел в виду? В любом случае, костыли - это не то, что я ожидал получить, заплатив 800 долларов за Windows NT и MSVC++ .