Полноэкранные программы - Язык Паскаль и начала программирования

Программирование: введение в профессию. 1: Азы программирования - 2016 год

Полноэкранные программы - Язык Паскаль и начала программирования

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

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

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

Возможности полноэкранных алфавитно-цифровых пользовательских интерфейсов далеко не так скромны, как может показаться на первый взгляд; помимо знакомых нам редакторов текстов, работающих в терминале, можно отметить, например, почтовый клиент mutt, клиент протокола ХМРР (известного также как Jabber) mcabber и многие другие программы; но действительно адекватное представление об открывающихся возможностях даёт игра NetHack, в которую некоторые люди играют десятилетиями (буквально так; известны случаи полного прохождения этой игры в течение 15 лет) и которая построена именно как полноэкранное алфавитно-цифровое приложение.

Умение работать с терминалом “на всю катушку” действительно позволяет создавать динамические игровые программы, пусть и не графические, но от этого ничуть не менее интересные; и, разумеется, этим дело не ограничивается.

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

2.11.1. Немного теории

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

Для написания полноэкранных программ этого недостаточно, поскольку драйвер терминала по умолчанию работает с клавиатурой в так называемом каноническом режиме, в котором, во-первых, активная программа получает пользовательский ввод строка за строкой, то есть эффект от нажатия какой-нибудь клавиши проявится не ранее, чем пользователь нажмёт Enter, и, во-вторых, некоторые комбинации клавиш, такие как Ctrl-C, Ctrl-D и прочее, имеют специальный смысл, так что программа не может их “прочитать” как обычную информацию и получает только уже готовый эффект — для Ctrl-C это сигнал SIGINT, для большинства программ фатальный, для Ctrl-D это имитация ситуации “конец файла” и т.д. К счастью, все эти особенности поведения драйвера терминала могут быть перепрограммированы, что полноэкранные программы и делают.

Если с перепрограммированием драйвера терминала всё более-менее ясно (достаточно прочитать справочную информацию по слову termios), то с escape-последовательностями всё оказывается сложнее. Дело в том, что терминалы когда-то выпускались весьма разнообразные, и наборы escape-последовательностей для них несколько различались; современные программы, эмулирующие терминал, такие как xterm, konsole и прочие, тоже отличаются друг от друга по своим возможностям. Все эти сложности более-менее покрывается библиотеками, предоставляющими некий набор функций для управления экраном терминала и генерирующими разные escape-последовательности в зависимости от типа используемого терминала. При программировании на языке Си для этих целей обычно используется библиотека ncurses, сама по себе довольно сложная.

В версиях Паскаля фирмы Борланд, популярных в эпоху MS-DOS, был предусмотрен специальный библиотечный модуль crt34, который позволял создавать полноэкранные текстовые программы для MS-DOS. Конечно, это не имело ничего общего с терминалами в Unix, управление экраном производилось то через интерфейс BIOS, то вообще путём прямого доступа к видеопамяти. Сейчас всё это представляет интерес разве что археологический — за исключением того, что создатели free Pascal поставили себе в качестве одной из целей добиться полной совместимости с TurboРascal’ем, включая и модуль crt; в результате та версия free Pascal, которую мы с вами используем в unix-системах, содержит свою собственную реализацию модуля crt. Эта реализация поддерживает все те функции, которые присутствовали в MS-DOS’овском Turbo Pascal’e, но реализует их, естественно, средствами escape-последовательностей и перепрограммирования драйвера терминала.

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

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

Прежде чем мы начнём обсуждение возможностей модуля, стоит сделать одно предостережение. Как только программа, написанная с использованием модуля crt, будет запущена, она тут же перепрограммирует терминал под свои нужды; кроме прочего, это означает, что спасительная комбинация Ctrl-C перестанет работать, так что если ваша программа “зависнет” или вы просто забудете предусмотреть корректный способ объяснить ей, что пора завершаться, то придётся вспоминать, как убивать процессы из соседнего окошка терминала. Возможно, стоит перечитать § 1.4.7.

Учтите, что для программ, написанных с использованием модуля crt, перенаправления ввода-вывода напрочь лишены смысла; всё это просто не будет работать.

2.11.2. Вывод в произвольные позиции экрана

Начнём с того, что очистим экран, чтобы текст, оставшийся от предыдущих команд, не мешал нашей полноэкранной программе. Это делается процедурой clrscr, название которой образовано от слов clear screen. Курсор при этом окажется в верхнем левом углу экрана; именно в этом месте появится текст, если сейчас его вывести с помощью обыкновенного write или writeln. Но мы этого делать не будем; гораздо интереснее самим указать то место на экране, куда должно быть выведено сообщение.

Текст, как известно, появляется там, где стоит курсор. Переместить курсор в произвольную позицию экрана позволяет процедура GotoXY, принимающая два целочисленных параметра: координату по горизонтали и координату по вертикали. Началом координат считается верхний левый угол, причём его координаты (1, 1), а не (0, 0), как можно было бы ожидать. Узнать ширину и высоту экрана можно, обратившись к глобальным переменным ScreenWidth и SreenHeight. Эти переменные тоже вводятся модулем crt; при старте программы модуль записывает в них актуальное количество доступных нам знакомест в строке и самих строк.

Имея в своём распоряжении GotoXY, мы уже можем сделать кое-что интересное. Напишем программу, которая показывает нашу традиционную фразу “Hello, world!”, но делает это не в рамках нашего диалога с командным интерпретатором, как раньше, а в центре экрана, очищенного от всего постороннего текста. Выдав надпись, уберём курсор обратно в левый верхний угол, чтобы он не портил картину, подождём пять секунд (это можно сделать с помощью процедуры delay, аргументом которой служит целое число, выраженное в тысячных долях секунды; она тоже предоставляется модулем crt), снова очистим экран и завершим работу. Длительность задержки, а также текст выдаваемого сообщения мы вынесем в начало программы в виде именованных констант.

Осталось вычислить координаты для печати сообщения. Координату по вертикали мы получим просто как половину высоту экрана (значение ScreenHeight, поделенное пополам); что касается координаты по горизонтали, то из ширины экрана (значения ScreenWidth) мы вычтем длину нашего сообщения, а оставшееся пространство, опять же, поделим пополам. При таком подходе различие между верхним и нижним полем, как и между правым и левым, не будет превышать единицы; лучшего нам всё равно не достичь, ведь вывод в алфавитно-цифровом режиме возможен только в соответствии с имеющимися знакоместами, сдвинуть текст на половину знакоместа не получится ни горизонтально, ни вертикально. Кстати, не следует забывать, что деление нам тоже потребуется целочисленное, с помощью операции div.

Итак, пишем:

Отметим, что текущие координаты курсора можно узнать с помощью функций WhereX к WhereY; параметров эти функции не принимают. Если мы с помощью GotoXY попытаемся переместить курсор в существующую позицию, то именно в этой позиции он и окажется, тогда как если мы попробуем его переместить в позицию, которой на нашем экране нет, получившиеся текущие координаты будут какие угодно, но не те, которых мы ожидали. Кроме GotoXY текущую позицию курсора, естественно, меняют операции вывода (обычно совместно с модулем crt используется оператор write).

К сожалению, у описанных средств есть очень серьёзное ограничение: если пользователь изменит размеры окна, в котором работает программа, то она об этом не узнает; значения ScreenWidthк ScreenHeight останутся теми, какими их установил модуль crt при старте. Источник этого ограничения вполне очевиден: в те времена, когда придумывали модуль crt, экран не мог изменить размер.

2.11.3. Динамический ввод

Для организации ввода с клавиатуры по одной клавише, а также для обработки всяких “хитрых” клавиш вроде “стрелочек”, F1 — F12 и прочего в таком духе модуль crt предусматривает две функции: KeyPressed и ReadKey. Обе функции не принимают параметров. Функция KeyPressed возвращает логическое значение: true — если пользователь успел нажать какую-нибудь клавишу, код которой вы пока не прочитали, и false — если пользователь ничего не нажимал; функция ReadKey позволяет получить код очередной нажатой клавиши. Если вы вызвали ReadKey раньше, чем пользователь что-то нажал, функция заблокируется35 до тех пор, пока пользователь не соизволит что-нибудь нажать; если же клавиша уже была нажата, функция вернёт управление немедленно.

Тип возвращаемого значения функции ReadKey — обыкновенный char, причём если пользователь нажмёт какую-нибудь клавишу с буквой, цифрой или знаком препинания, то ровно этот символ и будет возвращён. Примерно так же обстоят дела с пробелом (’ ’), табуляцией (#9), клавишей Enter (#13; обратите внимание, что не #10, хотя в какой-нибудь другой версии может получиться и #10), Backspace (#8), Esc (#27). Комбинации Ctrl-A, Crtl-B, Ctrl-C, ..., Ctrl-Z дают коды 1, 2, 3, ..., 26, комбинации Ctrl-[, Ctrl-\ и Ctrl-] позволяют получить следующие за ними 27, 28 и 29.

Несколько сложнее дело обстоит с остальными служебными клавишами, такими как “стрелочки”, Insert, Delete, PgUp, PgDown, F1-F12. Во времена MS-DOS создатели модуля crt приняли довольно неочевидное и не вполне красивое решение: использовали так называемые “расширенные коды”. На практике это выглядит так: пользователь нажимает клавишу, в программе функция ReadKey возвращает символ #0 (символ с кодом 0), что означает, что функцию необходимо вызвать во второй раз; символ, возвращённый функцией при этом повторном вызове (а управление она при этом возвращает немедленно), как раз идентифицирует нажатую специальную клавишу. Например, “стрелка влево” даёт коды 0-75, “стрелка вправо” — коды 0-77, “стрелка вверх” и “стрелка вниз” 0-72 и 0-80 соответственно. Следующая простая программа позволит вам выяснить, каким клавишам соответствуют какие коды:

Для завершения работы этой программы нужно нажать пробел.

Версия Free Pascal, имевшаяся у автора этих строк на момент написания книги, некоторые клавиши обрабатывала довольно странно, выдавая последовательность из трёх кодов, последним из которых был код 27 (Escape). Выделить такую последовательность очень сложно, ведь она даже не начинается с нуля. Такое поведение демонстрировали клавиша End, цифра 5 на дополнительной клавиатуре при выключенном NumLock, комбинация Shift-Tab. Кроме того, комбинации Ctrl-пробел к Ctrl-@ выдавали код 0, за которым ничего не следовало. Всё это противоречит исходной спецификации функции ReadKey из турбопаскалевского модуля crt и нигде никак не документировано. Вполне возможно, что при использовании других оконных менеджеров и вообще другой среды исполнения проявятся ещё какие-нибудь несообразности.

Чтобы получить общее представление относительно возможностей, которые открывает динамический ввод, мы напишем для начала программу, которая, как и hellocrt, будет выводить в середине экрана надпись “Hello, world!”, но которую потом можно будет двигать клавишами стрелок; выход из программы будет производиться по любой клавише, имеющей обычный (не расширенный) код.

В секции констант у нас останется только текст сообщения. Для выдачи сообщения и его убирания с экрана мы напишем процедуры ShowMessage и HideMessage; последняя будет выдавать в нужной позиции количество пробелов, равное длине сообщения.

Основой программы станет сравнительно короткая процедура MoveMessage, принимающая пять параметров: две целочисленные переменные — текущие координаты сообщения на экране; само сообщение в виде строки; два целых числа dx и dy, задающих изменение координат х и у.

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

У этой программы имеется серьёзный недостаток: она не отслеживает допустимые значения для координат, так что мы легко можем “вытолкать” сообщение за пределы экрана; после этого оно будет всегда появляться в левом верхнем углу. Исправить это предложим читателю в качестве упражнения.

Рассмотрим более сложный пример. Наша следующая программа выведет в середине пустого экрана символ “звёздочка” (*). Сначала символ будет неподвижен, но если нажать любую из четырёх стрелок, символ начнёт двигаться в заданную сторону со скоростью десять знакомест в секунду. Нажатие других стрелок изменит направление его движения, а нажатие пробела остановит. Клавиша Escape завершит программу.

В секции констант у нас снова появится DelayDuration, равная 100, то есть 1/10 секунды. Это интервал времени, который у нас будет проходить между двумя перемещениями звёздочки.

Учитывая опыт предыдущей программы, мы соберём все данные, задающие текущее состояние звёздочки, в одну запись, которую будем передавать в процедуры как var-параметр. Эти данные включают текущие координаты звёздочки, а также направление движения, заданное уже знакомыми нам значениями dx и dy. Тип для такой записи назовём просто star. Процедуры ShowStar и HideStar, получая единственный параметр (запись типа star) будут показывать звёздочку на экране и убирать её, печатая на этом месте пробел; процедура MoveStar будет смещать звёздочку на одну позицию в соответствии со значениями dx и dy. Для удобства опишем также процедуру SetDirection, которая заносит заданные значения в поля dx и dy; наконец, процедура HandleArrowKey будет в зависимости от переданного ей расширенного кода вызывать SetDirection с нужными значениями параметров. Коды пробела и Escape главная часть программы будет обрабатывать сама.

В главной части программы сначала будет производиться установка начальных значений для звёздочки и её вывод в середине экрана; после этого программа будет входить в псевдобесконечный цикл, в котором, если пользователь не нажимал никаких клавиш (то есть KeyPressed вернула false) будет вызываться MoveStar и производиться задержка; поскольку в этом случае больше делать ничего не нужно, тело цикла будет досрочно завершаться оператором continue (напомним, что, в отличие от break, оператор continue досрочно завершает только одну итерацию выполнения тела цикла, но не весь цикл). При получении расширенного кода будет вызываться процедура HandleArrowKey, при получении символа пробела звёздочка будет останавливаться соответствующим вызовом SetDirection, и при получении Escape цикл будет завершаться с помощью break.

Всё вместе будет выглядеть так:

2.11.4. Управление цветом

До сих пор все тексты, появляющиеся в окне терминала в результате работы наших программ, были одного и того же цвета — того, который указан в настройках терминальной программы; но это вполне можно изменить. Современные эмуляторы терминалов, как и сами терминалы последних моделей (например, DEC VT340, производство которых было прекращено только во второй половине 1990-х годов), формировали на экране цветное изображение и поддерживали escape-последовательности, задающие цвет текста и цвет фона.

К сожалению, интерфейс нашего модуля crt раскрывает эти возможности не в полной мере; дело в том, что прообраз этого модуля из Turbo Pascal был расчитан на стандартный текстовый режим так называемых IBM-совместимых компьютеров, где всё было сравнительно просто: каждому знакоместу соответствовали две однобайтовые ячейки видеопамяти, в первом байте располагался код символа, во втором — код цвета, причём четыре бита этого байта задавали цвет текста, три бита — цвет фона, что делало возможным использование всего восьми разных цветов для фона и шестнадцати — для текста; ещё один бит, если его взвести, заставлял символ мигать. Даже возможности алфавитно-цифровых терминалов, выпускавшихся в те времена, были шире, не говоря о современных программах-эмуляторах.

Поскольку модуль crt, представленный в free Pascal, разрабатывался в первую очередь ради поддержки совместимости с его прообразом, его интерфейс повторяет особенности интерфейса прообраза и не предоставляет никаких более широких возможностей. В принципе, такие возможности можно было бы задействовать, используя модуль video, но работать с ним существенно сложнее, а задачи перед нами пока стоят исключительно учебные; пожалуй, если вы всерьёз хотите писать полноэкранные программы для алфавитно-цифрового терминала, будет правильнее изучить язык Си и воспользоваться библиотекой ncurses; впрочем, как вы вскоре убедитесь, возможностей модуля crt вполне достаточно для создания довольно интересных эффектов; в то же время освоить его гораздо проще.

Основных средств у нас здесь всего два: процедура TextColor устанавливает цвет текста, а процедура TextBackground — цвет фона. Сами цвета задаются константами, описанными в модуле crt; они перечислены в табл. 2.1. Следует обратить внимание, что для задания цвета текста можно использовать все 16 констант, перечисленных в таблице, тогда как для задания цвета фона можно использовать только восемь констант из левой колонки. Например, если выполнить

то слово “Hello” будет выведено жёлтыми буквами на синем фоне, и такая комбинация будет использоваться для всего выводимого текста, пока вы снова не измените цвет текста или фона. Кроме того, вы можете заставить выводимый текст мигать; для этого при вызове TextBackground нужно добавить к её аргументу константу Blink; можно сделать это с помощью обычного сложения, хотя правильнее было бы использовать операцию побитового “или”. Например, текст, выводимый на экран после выполнения TextColorHBlue or Blink), будет синего цвета и мигающим.

Таблица 2.1. Константы для обозначения цвета в модуле crt

для текста и фона


только для текста

Black

чёрный


DarkGray

тёмно-серый

Blue

синий


LightBlue

светло-синий

Green

зелёный


LightGreen

светло-зелёный

Cyan

голубой


LightCyan

светло-голубой

Red

красный


LightRed

светло-красный

Magenta

фиолетовый


LightMagenta

розовый

Brown

коричневый


Yellow

жёлтый

LightGray

светло-серый


White

белый

К сожалению, необходимо отметить один фундаментальный недостаток описанных средств. Установки цвета текста и фона сохраняют своё действие после завершения вашей программы, при этом в модуле crt не предусмотрено средств, позволяющих узнать, какой цвет текста к фона установлен сейчас (в частности, на момент запуска вашей программы). Как следствие, восстановить цвета по умолчанию мы не можем. Как правило, после завершения нашей программы окошко терминала придётся закрыть или по крайней мере “привести в чувство” командой reset.

Следующая программа демонстрирует все возможные комбинации цветов текста и фона, заполняя экран звёздочками, причём каждая вторая звёздочка мигает. Для выхода из программы следует нажать любую клавишу. За неимением лучшего программа перед завершением устанавливает чёрный фон и тёмно-серый цвет текста; конечно, это может не соответствовать исходным настройкам терминала.






Для любых предложений по сайту: [email protected]