24 июля 2008 г.

История одного проекта 3

Внимание!
Не пытайтесь повторить это самостоятельно!

...потому что лучше пару раз почесать затылок, склонившись над листом бумаги, и сразу увидеть Корень Зла. Иначе он обовьет ваше несчастное тело быстрее, чем вы что-то сможете сделать. И вам придется долго и упорно грызть это корневище, чтобы, наконец, выбратся из этого персонального ада.
А корень этот на вкус напоминает кусок гнилого кода.. Чем, собственно, он и является.

Хотя мне, наверное, следовало бы предупредить вас в самом начале, потому что сейчас речь пойдет о банальнейших проблемах производительности.
Итак, в прошлый раз мы построили интерфейс, представляющий текстовое поле в виде бесконечного прямоугольного массива символов. Так что можно идти переопределять процедуру прорисовки окна. Так, а раз достаточно просто перерисовать каждый отдельный символ, то почему бы при перерисовке просто не вызвать m*n раз процедуру, рисующую один-единственный символ? А теперь объявим эту процедуру абстрактной. Отлично, теперь мы имеем довольно общий шаблон редактора, который всего лишь надо параметризовать процедурой прорисовки. То есть теперь, в принципе, есть на что посмотреть! Да, мерзко писать код, не имея возможности запустить его и посмотреть, что получится. И мы порождаем подкласс, назовем его ColorApxEd, который просто рисует цветные буковки на цветном фоне:
Отлично, все работает! Но так не бывает. Этот скриншот был сделан намного позже. Вначале, конечно же, ничего не работало. Но мало-помалу смертельные баги были раздавлены, и на свет явился очень мерзкий и живучий таракан.

Дело в том, что (1) при нажатии любой клавиши перерисовывалось все окно, и (2) при перерисовке текст мелькал. "Приехали".
Что ж, лучше поздно, чем никогда, наконец, взяться за ум. Тут-то я и узнал, что же такое двойная буферизация. А также понял, что прорисовку необходимо выполнять с умом - ровно то, что нужно, и ничего более. Еще одна неделя работы. Двойную буферизацию я прикрутил быстро, и все потихоньку начало работать.
И можно было, наконец, писать градиентную подсветку.

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

Но через два года, этим летом, я решил покопаться в старых наработках. И наткнулся на APXN. И ужасно удивился, что "почти всё почти работает". К тому же, на новом компьютере скорость перерисовки была вполне приемлемой. Кстати, новый компьютер с первого взгляда и помедленнее должен быть - 1600 MHz против старых 2400. Вот что значит Intel Core Duo, даже и не "2", да и программа многопоточность не использует.
И я просто занялся шлифовкой того, что было. Решил сделать "конфетку" из "будто бы и непригодного материала". И если раньше я пытался в нем что-то написать, и инстинктивно пытался Ctrl+прыгать по тексту, а также тыкать Ctrl+Del, и оно не работало, и я тут же проклинал эту гадость и возвращался к Notepad++, то после того, как я реализовал полный спектр клавиатурных команд, стало можно с ним работать! Вроде мелочь, а ведь именно из мелочей складывается настроение. А еще я поленился прикручивать поддержку одновременного открытия нескольких файлов, но заменил это кое-чем, не встречающимся в большинстве "блокнотов" - программа запоминает состояние (позицию прокрутки, каретку, закладки...) для каждого файла. Заодно вынуждает почаще сохраняться (в пику проклятому n++, не раз терявшему бесценные байты при неожиданной смерти системы :)
Теперь я уверовал в истинную силу юзабилити, когда пара строк кода просто делает работу возможной. А еще пара строк - приятной.


P. S. Только что прочитал одну отвратительно написанную книжечку по методам программирования, и с ужасом понял, что сам пишу так же. Так что не обращайте внимания на эти три небольшие заметки.
Впрочем, судите строго.

21 июля 2008 г.

История одного проекта 2

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

struct ApxString {
    char *chars; // строка символов
    short *styles; // строка стилей символов
    size_t size; // выделенная длина строк
    size_t length; // длина текста в строке
} buffer[]; // сам буфер в виде масива строк
size_t bufsize; // размер буфера
Возможно, именно в этом заключалась моя главная ошибка. Ведь для работы с этой структурой совершенно невозможно было использовать никакие библиотечные функции, хотя бы потому, что необходимо было параллельно каждой манипуляции со строкой символов делать то же со строкой стилей (которые еще short по давно забытой причине, хотя char'а бы вполне хватило).
Первым делом я решил абстрагироваться от тонкостей управления памятью. Это было достигнуто введением свойств CharAt[Row, Col], StyleAt[Row, Col] и RowLengths[Row, Col]>, которые, соответственно, позволяют прочитать или установить символ, стиль или использованную длину строки. Причем эти свойства ведут себя корректно при любых значениях параметров, автоматически перераспределяя память, если необходимо, и игнорируя отрицательные значения. Если символ не определен, то возвращается #0, если стиль не определен - возвращается стиль конца строки. Полная иллюзия бесконечного прямоугольного массива символов и стилей. Конечно, во всякой нетривиальной абстракции есть дыры. В данном случае дыра - это ограниченность памяти. Но я решил пренебречь этим ограничением, поскольку мегабайт текста в этой структуре будет занимать порядка 10MB: по 3 байта на символ, плюс 8 байт на длины, плюс запас на округление длины каждой строки вверх, скажем, до 20 символов. Так что все действительно не так уж страшно.
Осталось построить интерфейс для доступа тексту как к одной строке. Для этого я просто написал функции преобразования индексов - из двумерных в одномерный и обратно. Теперь для доступа к n-му символу нужно написать что-то вроде CharAt[IndexToPos(n)].

Выглядит неплохо, не так ли? Нет! Здесь допущена принципиальная ошибка: интерфейс удобен для самого компонента, но не для тех кто им пользуется. Никому не интересно внутреннее представление текста. Им нужен сам текст. И возможность работы с этим текстом как с обычной строкой. Поэтому более правильным, как мне сейчас кажется, было бы хранение текста в виде непрерывного массива с линейной адресацией, с каким-нибудь умным механизмом отображения символов на прямоугольную решетку.
В широко известном компоненте Scintilla применяется именно список строк, видимо, по соображениям производительности.

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

Теперь, когда появились высокоуровневые средства манипуляции текстом, можно строить слой обработки действий пользователя. Фактически вся работа делается в обработчиках KeyDown и KeyPress, первый из которых вызывается для всех клавиш, а второй - для символьных и некоторых командных (например, я очень удивился, когда узнал, что Ctrl+A - на самом деле символ с кодом 1).
Вот тут писать пришлось много. Вот лишь неполный список того, на что должно реагировать уважающее себя текстовое поле:

  • символьные клавиши
  • Enter
  • Delete и Backspace
  • PageUp/PageDown
  • Стрелки
  • Ctrl+Delete и Ctrl+Backspace
  • Ctrl+Стрелки
Некоторые из этих команд настолько естественны, что их наличие даже не осознаешь. Например, Ctrl+Backspace. Естественно, когда я начал пользоваться собственным редактором, тут же выявил кучу недоделок. Eat your own dog food!.

А как же градиентная прорисовка, из-за которой, собственно, все и затевалось? Об этом чуть позже.

19 июля 2008 г.

История одного проекта 1

Хочу поделиться с вами частью своего небольшого опыта разработки ПО. А именно, расскажу вам историю реализации одной бредовой идеи, посетившей меня в конце 11 класса, когда я еще не осознавал, насколько это сложно - создавать законченный программный продукт.
(Сам "продукт" в скором времени будет доступен в открытых исходниках).
В то время я немного увлекался компьютерной графикой, а также потихоньку делал программку для защиты на экзамене по информатике (тогда я и начал понимать, что такое законченный программный продукт). Программка та не представляет никакого интереса даже для меня, важны те условия, в которых я ее создавал - Borland Delphi 7 с ее весьма своеобразным редактором кода. Я очень долго ковырялся в цветах подсветки синтаксиса и как-то раз мне в голову пришла идея.
"А почему бы не сделать такую подсветку, где цвет фона текста меняется не резко, а плавно? Ведь на это же, наверное, намного приятней смотреть!"
И я решил сделать компонент, который предоставлял бы такую подсветку.

И тут же возникла первая проблема. Готового текстового редактора, который позволил бы управлять прорисовкой своего фона, у меня не было. Как присобачить подсветку синтаксиса к обычному текстовому полю, я тогда еще не знал (впрочем, и сейчас я знаю, что это нехорошо). А в интернете поискать не догадался, верите ли.
Так что я принял решение написать редактор с нуля. С абсолютного делфионского нуля, точнее, с TCustomControl'а, который всего лишь предоставляет окно и поверхность для рисования. Естественно, образцом для подражания был пресловутый делфионский редактор.
Что ж, дурному коту, как и бешеной собаке, семь верст не крюк - я принялся за работу.

Задумывались ли вы когда-нибудь, как устроено обыкновенное текстовое поле, такое как в Блокноте? Скорее всего да, поскольку если вас не отпугнуло начало моего рассказа, то, наверное, вы знакомы с компьютером если не на уровне "физиолога", то на уровне "терапевта" уж точно. И я рад за вас, если этот "простенький механизм" для вас действительно простенький. Мне он таковым не кажется до сих пор.
А мне предстояло реализовать нечто подобное самому, от начала и до конца. Странно, но я не испугался. "Меньше знаешь - крепче спишь". Ведь и Линус Торвальдс, как он пишет в своей книге "Just For Fun", признается, что если бы он заранее знал, сколько работы ему предстоит проделать, врядли бы взялся за свой проект. Впрочем, у незнания всегда есть и негативные последствия, причем, к сожалению, они обычно преобладают.

Но вернемся к нашим баранам.
Целью было создание VCL-компонента, аналогичного делфионскому редактору кода, с подсветкой синтаксиса, свободной кареткой и т.д., отличающегося "революционным" графическим дизайном. А также, разумеется, создание простенького редактора на основе этого компонента. Целью не являлось создание редактора общего назначения, а лишь редактора кода. Целевая аудитория - все, кому надоело писать ad hoc программки в "блокноте", и кто, к тому же, любит вещицы с переподвыпердовертом. (Такие как я :)
Раз уж я делаю редактор кода, то можно ограничиться моноширинными шрифтами. А следовательно - просто разбить компонент на прямоугольные ячейки одинакового размера. Это во много раз упрощает задачу.
Остается лишь реализовать "текстовый" интерфейс к этой прямоугольной решетке и должным образом редактировать решетку при нажатии клавиш. Всё!

А вот о том, что же я сделал, и почему это неправильно, я напишу завтра.

15 июля 2008 г.

Два подхода к разрешению неоднозначностей

Сегодня меня попросили разобраться с весьма странной проблемой: мобильный телефон при входящем вызове напрочь отказывался показывать имя, соответствующее номеру в телефонной книжке. Совершенно непонятно было, чем такое может быть вызвано. Оказалось, что проблемы возникли из-за повторяющихся номеров в книжке - когда поступал вызов, телефон отказывался решать, какое же из имен показывать, и не показывал имени вообще. Назовем подобное поведение "политикой Nokia". А "политикой Sony Ericsson" назовем выбор первого попавшегося имени.

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

АспектNokiaSony Ericsson
Мотивация Пользователь умнее машины, и только ему должно принадлежать право принимать окончательное решение. У машины достаточно данных, чтобы избавить пользователя от лишней головной боли.
Что делаем? Либо отказываемся выполнять противоречивое указание, требуя более корректной команды, либо запрашиваем уточнение. Разрешаем противоречие, используя имеющиеся данные и немного машинного здравого смысла ;)
Достоинства У пользователя есть возможность исправить команду и получить корректный результат. Система ведет себя предсказуемо, и пользователь не испытывает дискомфорта из-за того, что кто-то пытается его перехитрить. Пользователь экономит кучу времени на ответах на тривиальные или, того хуже, несущественные, вопросы.
Недостатки Пользователя порой отвлекают по пустякам, что не может не раздражать. Одна секунда, потраченная на ответ на возникший из ниоткуда дурацкий вопрос, типа "Установить критические обновления?" может стоить 15 минут рабочего времени из-за потери концентрации. Автоматика может натворить чего-нибудь нехорошее (например, повесить систему в ходе фоновой установки этих самых критических обновлений). Кроме того, многим может не понравиться, что компьютер вообразил себя умнее них.

Отсюда сразу видны приблизительные границы применимости этих подходов. Ясное дело, если от принятого решения зависит что-то "материальное" (например возможность перевести деньги не на тот счет), лагерь Nokia имеет право бесконечно долго пытать пользователя, заставляя убрать весь "мусор" из окна ввода номера счета, вроде лишних букв; тогда как решение об отображении хоть какой-нибудь подсказки о том, кто вам звонит посреди ночи, в любом случае лучше, чем не сказать ничего кроме номера (особенно если вы, так же как и я, не помните ни одного телефонного номера).
Что же касается вышеупомянутой установки критических обновлений - я вполне согласен с наличием закопанной в настройках опции "автоматически устанавливать". Тут скорее дело вкуса.


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

14 июля 2008 г.

Антистандартизация: еще один болт

Сколько видов шлицев на болтах вы знаете? Всякий, кто брал в руки отвертку, скажет, что они бывают плоские и крестовые. И на протяжении десятилетий, имея небольшой наборчик из десятка отверток, можно было завинтить любой болт! Но все в мире меняется...
Немного покопавшись в Сети, легко находим следующую информацию (источник; кстати, на том же сайте рассказывается о происхождении крестообразного шлица):

Сегодня в практике наиболее широко встречаются следующие виды шлицев: Прямой; Крестообразный типа Phillips (согласно стандартам DIN - крестообразный типа Н); Крестообразный типа Pozidriv (согласно стандартам DIN - крестообразный типа Z); Комбинированный типа Pozidriv + прямой; Шестиконечная звезда типа Тоrх; Внутренний шестигранник; Шестигранный.

И только за последние 3 дня я повидал 5 видов шлицев, двух из которых в этом списке нет, а именно, фирменный "трехгранный" Nokia и, о ужас, подобие ставшей уже обычной шестиконечной звезды, но со штырем внутри, так что нужна полая отвертка. Которой, ясное дело, ни у кого нет. Я было хотел нарисовать чертеж этого творения Ада, но прокопавшись по 20 минут с Expression Design и Inkscape, понял, что лучше мне этого не делать :-[

Итак, в чем же дело, почему под угрозой деталь, которая была стандартизована одной из первой, и которая применяется буквально везде - шуруп?
Я вижу две возможных причины. Одна - разумная, другая -... тоже.

  1. Новая форма шлица ускоряет производство;
  2. Новая форма шлица затрудняет разбор не предназначенного для того изделия "криворуким пользователем" (своего рода обфускация)
У креста есть некоторые преимущества перед плоским шлицем, равно как и у звездочки и шестигранника - перед крестом (существенно труднее сорвать шлиц). При этом пока еще не у каждого дома лежит набор "звездочек". Так что заодно "защищаемся от дурака". Тогда как ужасная "звезда-со-штырем" явно не дает технологического преимущества, зато... впрочем, истинного "энтузиаста" ничто не способно защитить от удара током.
А самое смешное, что вышеупомянутый болт был применен на элементарнейшем устройстве - нагревателе для детского питания, который любой мужик без труда отремонтирует. Если бы не этот корень зла.
Так что налицо признание безрукости пользователей во всех областях.
И это весьма печально.

Beginning of The Construction

Приветствую всех на этой пустынной странице! Я наконец-то собрался с силами (к сожалению, не с мыслями) и, следуя совету Джефа Этвуда (Jeff Atwood), завел блог. Захотелось потренироваться в написании статей, потому что хороший разработчик должен уметь писать. Посему любая критика "категорически приветствуется". Придирайтесь на здоровье!
Чтобы вам не было совсем скучно, я заготовил несколько ссылок, чтобы вы легко могли сбежать отсюда куда-нибудь поинтереснее :) Joel on Software - Joel Spolsky пишет о всех аспектах разработки ПО (типа shrinkwrap). Coding Horror - Programming and human factors by Jeff Atwood The Old New Thing - Известнейший разработчик Microsoft Реймонд Чен (Raymond Chen) об истории развития Windows API, а также о многом другом, например, о вьетнамских сэндвичах. Ну все, хватит о разработке ПО. Кстати, похоже, что это будет основной темой блога... Сайт Кори Доктороу, того самого писателя-фантаста, который написал Землю Сисадминов.
Ах да, я забыл представиться. Антон Григорьев, студент МФТИ, mailto: ansgri at gmail dot com See ya!