Программирование консольных приложений
Интерес к написанию консольных приложений Win32 довольно устойчив. Эти компактные и шустрые программки чем-то удивительно симпатичны. А в ряде случаев консольный режим вообще незаменим. В условиях отсутствия доступных справочных материалов - эта статья, возможно, поможет начинающему программисту "текстового экрана".
Общие сведения о консольном режиме Консоль
Программа текстового режима пишется, по существу, аналогично программе для DOS, поэтому придется вспомнить подзабытый опыт программирования на Turbo Pascal. В основном используются вызовы функций Win32 API из модуля Windows. Вероятно, пригодятся и утилиты из SysUtils. В отдельных случаях (для обработки сообщений Windows) потребуется модуль Messages.
Без нужды не следует использовать модули Classes и Forms, иначе размер исполняемого файла разрастется во много раз, потеряются существенные преимущества - компактность, быстрота и четкость работы. Консоль с точки зрения программиста состоит из входного буфера и одного или нескольких экранных буферов.
Входной буфер - это очередь записей с информацией о событиях, относящихся к вводу, а именно:
- нажатие и отпускание клавиш;
- события от мыши (перемещение, нажатие и отпускание кнопок) - если они разрешены;
- изменение размера активного экранного буфера - если разрешено.
Экранный буфер - это двумерный массив (80х25) символов и их атрибутов (цвет символа и фона) для вывода в консольное окно. Для доступа к консоли в Win32 API имеются функции как высокого, так и низкого уровня. Вторые предоставляют более гибкие возможности. Разумеется, серьезный программист должен будет обращаться к описаниям функций в справочной системе Delphi (точнее, Windows SDK), хотя читать эти английские тексты с объявлениями на Си не всякому "дельфийцу" покажется легко. Да и документировано далеко не все. Потому-то и родилась эта статья.
Работа консольных программ
Общие настройки консольных программ находятся в системном реестре под ключом HKEY_CURRENT_USER\CONSOLE. Некоторые программы могут иметь индивидуальные настройки, хранящиеся в HKEY_CURRENT_USER\CONSOLE\имя программы. Этот субключ создается автоматически при изменении свойств консольного окна (доступ через системное меню окна с выполняющейся программой). Индивидуальные настройки имеют приоритет. В частности, должна ли программа выполняться в полноэкранном режиме - определяет параметр FullScreen, равный 1 (или 0 - для оконного режима). Конечно, этот абзац относится только к системам класса NT. В системах класса 95/98 все хуже: за ввод-вывод при работе консольных приложений отвечает DOS-программа conagent.exe, которую можно найти в папке Windows/System. Настройка режима сохраняется в pif-файле этой программы. Указанная программа эмулирует консоль NT, и делает это не лучшим образом. Так что, если предполагается запускать разрабатываемую программу в подобных ОС, ее следует внимательно тестировать, особенно работу мыши. И проверять настройки pif-файла, от них тоже зависит результат.
Создание консоли
Для создания консоли (в том числе и из приложения GUI) используют функцию AllocConsole. Эта функция назначает дескрипторы стандартного ввода, вывода и обработчика ошибок. Процесс может иметь только одну консоль, поэтому повторный вызов функции AllocConsole даст False. Можно задать заголовок окна (когда консоль исполняется в оконном режиме): SetConsoleTitle( 'Super Program' ); Иначе - в заголовке будет просто имя файла. Для полноэкранного режима от этого оператора толку нет. Завершение консольного сеанса - вызов FreeConsole. Если в файле проекта Delphi использована директива {$APPTYPE CONSOLE} , то о создании и освобождении консоли заботиться излишне - это будет сделано автоматически.
Дескрипторы
Вызовом GetStdHandle можно узнать дескрипторы (handles) буфера ввода, стандартного экранного буфера и обработчика ошибок (не потребуется). Функция SetStdHandle может, при необходимости, переназначить их - это понадобится лишь при создании порожденных консольных процессов.
Обработка системных событий
По умолчанию такие события, как нажатие Ctrl+C, Ctrl+Break интерпретируются как системные, вызывающие немедленное завершение консольной программы. Аналогично действует, например, простой клик на "крестике" окна.
Можно переназначить эти события вызовом: SetConsoleCtrlHandler(@Proc,True);
Тогда при нажатии указанных комбинаций будет выполняться определенная в программе процедура Proc (имя может быть любое). Даже если эта процедура пустая - умолчание все равно уже не будет действовать. То же относится и к попытке просто закрыть окно Windows. Между прочим, можно вообще отменить особую обработку сочетания Ctrl+C - см. об этом дальше.
Если мы хотим, чтобы после завершения Proc были все же выполнены действия по умолчанию, оформляем ее как функцию, возвращающую False.
В Proc может быть произведен анализ типа системного события, чтобы реагировать на каждое по-своему. Например, так: procedure Proc(SysEv: Cardinal);
begin
case SysEv of
CTRL_BREAK_EVENT: ... // если нажали Ctrl+Break
CTRL_CLOSE_EVENT: ... // если пытаются закрыть окно
CTRL_LOGOFF_EVENT: ... // если пользователь завершает сеанс
CTRL_SHUTDOWN_EVENT: ... // если система выгружается
end;
end;
Возникновение программных исключений повлечет выдачу стандартных системных предупреждений (если программа работала в полноэкранном режиме, она переключится при этом в оконный). Для предотвращения таких нештатных ситуаций - не стоит забывать применять в нужных местах известные операторы обработки исключений.
Режимы консоли
Для пользования высокоуровневыми средствами - режимы консоли установлены оптимально по умолчанию. Если же мы собираемся пользоваться низкоуровневыми вызовами, следует обратить внимание на функцию SetConsoleMode. Режимы вывода лучше не трогать, практически важен только режим ввода, а конкретно - два флага:
ENABLE_MOUSE_INPUT - принимать события мыши; ENABLE_PROCESSED_INPUT - разрешить системный статус для сочетания Ctrl+C. Примеры: SetConsoleMode(inpHnd,0); (не поддерживать мышь, обрабатывать Ctrl+C как обычное сочетание клавиш), SetConsoleMode(inpHnd,ENABLE_PROCESSED_INPUT+ENABLE_MOUSE_INPUT); (поддерживать мышь, системный статус для Ctrl+C). inpHnd - дескриптор буфера ввода: inpHnd:=GetStdHandle(STD_INPUT_HANDLE);
Вывод на экран Экранный буфер
Консоль может иметь и несколько экранных буферов, один из них - активный, т.е. отображается. Новый буфер создается так: Hnd1:=CreateConsoleScreenBuffer(GENERIC_READ+GENERIC_WRITE,
0,nil,CONSOLE_TEXTMODE_BUFFER,nil);
Возвращается дескриптор нового буфера. Как сделать буфер активным, т.е. показать на экране: SetConsoleActiveScreenBuffer(Hnd1);
Надо учесть, что новый активный буфер не становится стандартным выводом!
Вызовом CloseHandle можно ликвидировать дополнительный экранный буфер, если в нем отпадет нужда.
Координаты символа в буфере соответствуют отсчету от верхнего левого угла экрана, эта ячейка считается имеющей координаты (0, 0).
Экранный буфер включает также координаты своего курсора и свой текущий атрибут (об этом ниже).
Высокоуровневый вывод на экран
Для начала - замечание. Многие функции вывода для задания координат на экране (и в буфере) будут использовать переменные - записи, имеющие тип _COORD. Два поля этой записи (X и Y) несут значения координат по горизонтали вправо и по вертикали вниз.
Отметим ряд особенностей средств высокого уровня:
- Они всегда выводят текст, начиная с места нахождения курсора (текстового, не путать с указателем мыши).
- Они перемещают курсор (он оказывается, как правило, в конце выводимого текста, если только не перемещен служебными символами).
- Они выводят текст с текущим атрибутом.
- Занятое текстом место на экране может не соответствовать числу выведенных символов. Например, вывод #8 (BackSpace), #13 (CR), #10 (LF) вызовет только соответствующее перемещение курсора.
Атрибут для функций высокого уровня (текущий) задается вызовом SetConsoleTextAttribute. Например: SetConsoleTextAttribute(Hnd,7+1*16);
Здесь задан серый цвет (7) на синем фоне (1). По умолчанию атрибут обычно равен 7 (серый на черном). Это системная установка, она не зависит от программы и может быть изменена вне ее - в реестре.
Напомним, что атрибут включает цвета символа (foreground, разряды 1-4 младшего байта) и фона (background, разряды 5-8). В полноэкранном режиме для NT значимыми для фона будут 5-7 разряды, а единица в 8-м означает "мигание" (blink) - как в DOS.
Изменив текущий атрибут, мы не повлияем, конечно, на ранее выведенный текст.
В простейшем случае для вывода можно пользоваться известными паскалевскими операторами Write, Writeln. Этот способ применим только к стандартному выводу, так что, если активным сделан другой буфер, вывода мы не увидим - он пойдет на невидимый экран.
Довольно похож и вывод с помощью WriteConsole, но здесь писать можно на любой экран, в том числе не активный.
Рассмотрим пример: var
C: _COORD;
Hnd, Wr: Cardinal;
...
begin
...
Hnd:=GetStdHandle(STD_INPUT_HANDLE);
C.X:=26;
C.Y:=4;
SetConsoleCursorPosition(Hnd,С);
WriteConsole(Hnd,@Buf,10,Wr,nil);
...
Здесь сначала курсор устанавливается в точку с координатами (26,4), затем выводятся на экран 10 символов из буфера Buf, содержащего текст. Переменная Wr нужна для возвращения в нее количества записанных символов. Выводимый текст забивает то, что было в буфере под ним (подстилающий текст), меняя все атрибуты на текущий атрибут.
При пользовании высокоуровневыми функциями надо помнить, что они перематывают экран, когда доходят до его конца. Так что, записав символ в крайнюю правую нижнюю точку, не удивляйтесь, что она сделается уже не крайней. В принципе, такое поведение можно отключить, воспользовавшись SetConsoleMode для режима вывода.
Очевидно, что для создания развитого экранного интерфейса средства высокого уровня не слишком удобны.
Низкоуровневый вывод в экранный буфер
Особенности низкоуровневых средств:
- Они не влияют на положение курсора и не зависимы от него.
- Они никак не связаны с текущим атрибутом (о котором говорилось выше).
- На экране занимается ровно столько места, сколько символов выводится.
Выводим только символы, не меняя атрибутов подстилающего текста: WriteConsoleOutputCharacter(Hnd,Buf,n,C,Wr);
Текст выводится из буфера Buf, берется n символов. Вывод идет, начиная с позиции, задаваемой переменной C. Вывод на правый край вызовет перенос на следующую строку, но окно не прокручивается никогда. То, что вылезло за нижний правый угол, будет урезано (но лучше этого избегать). Вывод вне рабочей области координат (80 колонок, 25 строк) не даст эффекта.
А вот так можно заполнить 10 последовательных ячеек, начиная с C, заданным атрибутом (ярко-белый текст на красном фоне), не меняя текста: FillConsoleOutputAttribute(Hnd,15+4*16,10,C,Wr);
Так можно, например, очистить от текста блок из 10 последовательных символов, начиная с позиции C: FillConsoleOutputCharacter(Hnd,#0,10,C,Wr);
Ноль на экране эквивалентен пробелу. Вывод только строки атрибутов, содержащихся в буфере Buf1, не меняя текста: WriteConsoleOutputAttribute(Hnd,@Buf1,10,C,Wr);
Управление курсором
Курсор - это принадлежность экранного буфера. Если буферов несколько, у каждого будет свой курсор со своим положением. Установить курсор в позицию экрана с координатами C: SetConsoleCursorPosition(Hnd,C);
Имеются и средства для изменения вида курсора, но чаще всего интересует способ скрыть курсор. Делается это так: var
CCI: _CONSOLE_CURSOR_INFO;
begin
GetConsoleCursorInfo(Hnd,CCI);
CCI.bVisible:=False;
SetConsoleCursorInfo(Hnd,CCI);
...
Присвоение True делает курсор снова видимым.
Структура экранного буфера
Каждая ячейка экранного буфера - это запись. Ее тип: CHAR_INFO. Поля этой записи: AsciiChar, UnicodeChar - символ в ячейке;
Attributes - цвета символа и фона. Смысл то же, что и в DOS.
Атрибут имеет тип Word, но актуален только младший байт. Поэтому старшие разряды можно использовать произвольно, помещая там какие-нибудь флаги или дополнительную информацию типа скрытого текста. Дальше в одном из примеров такая возможность будет задействована.
Скроллинг окна
На низком уровне можно реализовать прокручивание всего консольного окна или его части (например, терминального окошка, занимающего часть экрана). Для этого имеется мощная функция ScrollConsoleScreenBuffer, предназначенная для работы с прямоугольными блоками.
С ее помощью можно заданный прямоугольный блок экрана (символы с их атрибутами) переместить целиком на другое место, а "оголившуюся" область заполнить определенным символом и атрибутом.
В следующем примере производится прокрутка окошка шириной 12 и высотой 11, примыкающего к левому верхнему углу экрана. Прямоугольный блок (за исключением верхней строки) смещается вверх на 1, затирая верхнюю строку. Освободившаяся снизу строка заполняется атрибутом 7 (и по умолчанию символом #0). var
C_I: CHAR_INFO;
C1: _COORD;
R1: _SMALL_RECT;
...
R1.Left:=0;
R1.Top:=1;
R1.Right:=11;
R1.Bottom:=10;
C1.X:=0;
C1.Y:=0;
C_I.Attributes:=7;
ScrollConsoleScreenBuffer(OutHnd,R1,nil,C1,C_I);
В переменной R1 задаются координаты углов перемещаемого блока. В C1 - координаты левого верхнего угла для его нового положения. Переменная C_I определяет, чем заполнять освободившуюся область.
Ввод с клавиатуры Высокоуровневый ввод с клавиатуры
Высокоуровневые средства ввода не реагируют на служебные клавиши, и вдобавок несовместимы с мышью. Но для некоторых применений они бывают удобны.
О таких операторах как Read, Readln говорить вряд ли стоит. Впрочем, и они имеют свои плюсы: встроенную поддержку эха на экране и поддержку обработки BackSpace. А иначе (например, при реализации строки ввода) все это придется делать ручками, как и будет дальше показано.
Итак, начнем работать с буфером ввода, который представляет собой очередь записей о событиях. Сначала рекомендуется вызвать функцию FlushConsoleInputBuffer, которая произведет начальную очистку буфера (при старте программы он обычно не пуст).
Проверка входного буфера - неотъемлемая часть работы с ним. Существует функция GetNumberOfConsoleInputEvents, возвращающая число непрочитанных записей, но более красиво будет применить специальное системное средство Windows - так наз. wait-функцию WaitForSingleObjectEx. Эта функция непрерывно проверяет статус буфера и завершается, когда появляются непрочитанные записи.
Вот как реализована, например, строка ввода на 10 символов. var
InputStr: string[10]; // Принимающий буфер
BckSpSeq: array[0..2] of Char = (#8,#0,#8); // Последовательность для BackSpace
...
repeat
WaitForSingleObjectEx(InpHnd,INFINITE,Тrue); // Ожидаем нажатия
ReadConsole(InpHnd,@a,1,Wr,nil); // Читаем символ в "а"
if Length(InputStr)<10 then // не заполнено ли?
begin
if a>Chr(31) then InputStr:=InputStr+a; // Если значащий, то добавляем
if a=#8 then // Если BackSpace, то:
begin
Delete(InputStr,Length(InputStr),1); // Затираем последний в буфере
if Length(InputStr)>0 then
WriteConsole(OutHnd,@BckSpSeq,Length(BckSpSeq),Wr,nil);
// Затираем последний на экране, курсор назад
end else WriteConsole(OutHnd,@a,1,Wr,nil); // Иначе: эхо на экран
end;
until a = #13; // Enter - выход из цикла, обработка
Здесь организован цикл проверки входного буфера repeat ... until, и подобный цикл будет фигурировать всегда. В данном случае очередная запись из буфера ввода (точнее, символ) считывается в символьную переменную a (служебные клавиши и прочие события игнорируются). Значащие символы добавляем в принимающий буфер и параллельно выводим на экран как эхо. Курсор смещается автоматически, поскольку для вывода взята высокоуровневая функция WriteConsole.
Приходится озаботиться только обработкой клавиши BackSpace (#8). Ведь при ее нажатии надо не только ликвидировать последний символ в принимающем буфере, но сделать это и на экране. Для последнего - выводим последовательность трех символов, которая: 1) возвращает курсор назад; 2) выводом нуля затирает последний символ; 3) снова возвращает курсор назад.
Выход из цикла в этом примере реализован по нажатию Enter (#13). Далее последует обработка введенной строки.
Интересно, что можно и не предусматривать особого принимающего буфера, упростить программу, если брать в обработку текст прямо с экрана (из экранного буфера). Как это делать - разберемся позднее.
Буфер ввода
Переход к использованию низкоуровневых средств требует детального знания не слишком простого формата записей в буфере ввода. Эти записи имеют тип INPUT_RECORD. Описаны так: INPUT_RECORD = record
EventType: Word;
Reserved: Word;
Event: Record case Integer of
0: (KeyEvent: TKeyEventRecord);
1: (MouseEvent: TMouseEventRecord);
2: (WindowBufferSizeEvent: TWindowBufferSizeRecord);
3: (MenuEvent: TMenuEventRecord);
4: (FocusEvent: TFocusEventRecord);
end;
end;
- 1. EventType(тип Word) - тип события: KEY_EVENT - от клавиатуры; _MOUSE_EVENT - от мыши; WINDOW_BUFFER_SIZE_EVENT - об изменении размера экранного буфера (на практике не используется).
- reserved - не используется.
- Event(тип record), поля которого заполняются в зависимости от EventType.
Поскольку мы пока что интересуемся событиями от клавиатуры (EventType = KEY_EVENT), для нас актуально только поле Event.KeyEvent(тип _KEY_EVENT_RECORD). KEY_EVENT_RECORD = packed record
bKeyDown: BOOL;
wRepeatCount: Word;
wVirtualKeyCode: Word;
wVirtualScanCode: Word;
case Integer of
0: (
UnicodeChar: WCHAR;
dwControlKeyState: DWORD);
1: (
AsciiChar: CHAR);
end;
{Это - тоже запись, с полями:
bKeyDown: True, если клавиша нажата, False, если отпущена;
wRepeatCount: 1 (или больше - для повторных событий -
когда клавиша удерживается нажатой);
wVirtualKeyCode - виртуальный код клавиши;
wVirtualScanCode - виртуальный скан-код;
AsciiChar - символ;
UnicodeChar - символ в Unicode;
dwControlKeyState - состояние управляющих клавиш, комбинация констант:
CAPSLOCK_ON,
ENHANCED_KEY,
LEFT_ALT_PRESSED,
LEFT_CTRL_PRESSED,
NUMLOCK_ON, RIGHT_ALT_PRESSED,
RIGHT_CTRL_PRESSED, SCROLLLOCK_ON,
SHIFT_PRESSED,
смысл которых не требует пояснений.}
Далее будет показано, как со всем этим хозяйством работать.
Низкоуровневый ввод с клавиатуры
Итак, если мы хотим воспринимать не только буквы, а нажатия (и отпускания) любых клавиш (а впоследствии и события мыши), то функция ReadConsole уже не годится. Следует применять ReadConsoleInput. Ниже показан пример правильной организации цикла обработки ввода, который и образует ядро классической консольной программы. var
a: Char;
IR: INPUT_RECORD;
C: _COORD;
...
repeat
WaitForSingleObjectEx(InpHnd,INFINITE,false); // ждем события
ReadConsoleInput(InpHnd,IR,1,Wr); // берем запись в переменную IR
case IR.EventType of // анализируем тип события
KEY_EVENT: // от клавиатуры:
begin
if IR.Event.KeyEvent.bKeyDown then // если клавиша нажата
begin
a:=IR.Event.KeyEvent.AsciiChar; // взять символ в переменную "а"
if a>#0 then // если буквенная клавиша:
begin
WriteConsoleOutputCharacter(OutHnd,@a,1,C,Wr); // вывести букву на экран
Inc(C.X); // сместиться вправо
... // что-нибудь еще...
end else // если служебная:
case IR.Event.KeyEvent.wVirtualKeyCode of // проверяем код виртуальной клавиши
VK_UP: if (IR.Event.KeyEvent.dwControlKeyState and SHIFT_PRESSED <> 0)
then ... // если Shift+Вверх, то делаем что-то
...
end;
end;
end;
_MOUSE_EVENT: ... // от мыши: (обработка событий мыши)
end;
until IR.Event.KeyEvent.wVirtualKeyCode = VK_F10; // выход по нажатию F10
При нажатии служебных клавиш в поле AsciiChar будет ноль. Для программирования реакций на нажатие таких клавиш следует анализировать Virtual-Key Codes в поле wVirtualKeyCode, как и показано в примере. Константы этих кодов даны в Windows SDK.
Поддержка мыши События от мыши
События от мыши поддерживаются, только если вызовом SetConsoleMode разрешен режим мыши. В записях буфера ввода нас интересует теперь поле Event.MouseEvent(тип _MOUSE_EVENT_RECORD). _MOUSE_EVENT_RECORD = packed record
dwMousePosition: TCoord;
dwButtonState: DWORD;
dwControlKeyState: DWORD;
dwEventFlags: DWORD;
end;
{Это - запись с полями:
dwMousePosition - текущее положение мышиного указателя (в символах) относительно
левого верхнего угла, принятого за (0,0);
dwButtonState - состояние кнопок, комбинация понятных констант:
FROM_LEFT_1ST_BUTTON_PRESSED,
RIGHTMOST_BUTTON_PRESSED,
FROM_LEFT_2ND_BUTTON_PRESSED,
FROM_LEFT_3RD_BUTTON_PRESSED,
FROM_LEFT_4TH_BUTTON_PRESSED;
dwControlKeyState - аналогично клавиатуре;
dwEventFlags - тип события от мыши: 0 - нажатие или отпускание, DOUBLE_CLICK,
MOUSE_MOVED - понятно без пояснений. При двойном клике - сперва фиксируется
событие простого нажатия, потом - двойного клика.}
Обработка событий мыши
Собственно, теперь уже все должно быть понятно. Обработка событий от мыши ведется в том же цикле. Выше в примере для нее даже заготовлено место. Вот обработчик, реализующий перестановку текстового курсора на место клика: if IR.Event.MouseEvent.dwButtonState=FROM_LEFT_1ST_BUTTON_PRESSED
then SetConsoleCursorPosition(OutHnd,IR.Event.MouseEvent.dwMousePosition);
Приведем пример более сложной операции: выделение мышью текста. Для упрощения предполагается, что движение мыши, селектирующее текст, может быть только направо-вниз. Вот обработчик, который следует включить в цикл проверки входного буфера (в раздел _MOUSE_EVENT): if IR.Event.MouseEvent.dwButtonState
and FROM_LEFT_1ST_BUTTON_PRESSED = 0 then
begin
Cbeg:=IR.Event.MouseEvent.dwMousePosition;
Cend:=Cbeg;
end else SetSel(IR.Event.MouseEvent.dwMousePosition);
В глобальных переменных Cbeg и Cend типа _COORD хранятся координаты начала и конца выделения. Пока кнопка не нажата, они обе просто следят за текущим указателем мыши. Если кнопка прижата - вызывается процедура SetSel, в которую передаются координаты указателя (это будет конец нового блока выделения, а начало - Cbeg). //Описания вспомогательных функций:
function GetCount(C1,C2: _COORD): word;
// считает длину текста между точками
begin
Result:=(C2.Y-C1.Y)*80+C2.X-C1.X;
end;
//================================================
procedure DelSel; // снимает все выделение с экрана
var
Buffer: array[0..79,0..24] of Word;
C: _COORD;
i, j: integer;
begin
C.X:=0; // начало координат
C.Y:=0;
ReadConsoleOutputAttribute(OutHnd,@Buffer,80*25,C,Wr); // все атрибуты экрана в массив
for i:=0 to 79 do for j:=0 to 24 do
if Buffer[i,j]>$FF then Buffer[i,j]:=not Buffer[i,j]; // снимаем выделение
WriteConsoleOutputAttribute(OutHnd,@Buffer,80*25,C,Wr); // возвращаем на экран
end;
//=================================================
procedure SetSel(C: _COORD); // выделяет текстовый блок от Cbeg до С
var
Buffer: array of Word;
i, Len, n1, n2: integer;
begin
n1:=GetCount(Cbeg,Cend); // длина имеющегося выделения
n2:=GetCount(Cbeg,C); // длина нового
if n2<0 then Exit;
if n2>n1 then Len:=n2 else Len:=n1; // длина буфера - по максимуму
if Len>0 then
begin
SetLength(Buffer,Len); // готовим буфер
ReadConsoleOutputAttribute(OutHnd,@Buffer[0],Len,Cbeg,Wr); // читаем фрагмент с экрана
for i:=0 to Len-1 do if (Buffer[i]>$FF) then Buffer[i]:=not Buffer[i];
// снимаем старое выделение
for i:=0 to n2-1 do Buffer[i]:=not Buffer[i]; // делаем новое
WriteConsoleOutputAttribute(OutHnd,@Buffer[0],Len,Cbeg,Wr);
// выводим фрагмент на экран
Cend:=C; // обновляем конец выделения
end else DelSel; // просто щелчок - снять выделение
end;
"Изюминка" здесь в том, что для выделения используется инверсия атрибута. При этом старшие (нерабочие) разряды атрибута устанавливаются в 1. Таким образом, получается простой признак для проверки, выделена ли данная ячейка (тогда значение атрибута будет заведомо больше 255).
Продвинутые возможности Ввод с экрана
Экранный буфер - это ведь не только средство отображения. Он может служить промежуточным хранилищем для последующего ввода текста в обработку. Для получения текста с экрана служит вызов ReadConsoleOutputCharacter. Например: ReadConsoleOutputAttribute(OutHnd,@Buffer[0],12,C,Wr);
Здесь 12 последовательных символов читаются с экрана, начиная с позиции С, и записываются в Buffer.
Засылка в очередь
Занося в буфер ввода сформированные программно записи, можно тем самым имитировать клавиатурный ввод (как, впрочем, и другие события). В следующем примере решается задача воспроизвести цифру "7", как будто бы она была нажата на клавиатуре.
Для этого придется должным образом заполнить поля переменной (IR) типа INPUT_RECORD. Ряд полей можно игнорировать. Собственно, если обрабатывать записи будет наша же программа на низком уровне, то мы сами знаем, какие поля потребуются. Между прочим, поля, не принимаемые во внимание, можно использовать произвольно. Например, передавать в них флаг, сигнализирующий, что это "имитированные" нажатия.
Подготовленная запись засылается в конец очереди оператором WriteConsoleInput. Как видно из текста фрагмента программы, здесь засылаются даже две записи: первая имитирует нажатие клавиши "7", вторая - ее отпускание. Возможно, без второй можно и обойтись. with IR do
begin
EventType:=KEY_EVENT;
with Event.KeyEvent do
begin
bKeyDown:=True;
wRepeatCount:=1;
AsciiChar:= '7' ;
WriteConsoleInput(InpHnd,IR,1,Wr);
bKeyDown:=False;
WriteConsoleInput(InpHnd,IR,1,Wr);
end;
...
Вообще при работе на низком уровне можно более гибко использовать буфер ввода. Почему бы, например, не определить дополнительно собственный тип событий, чтобы обрабатывать в цикле и его тоже? Например, программа - обработчик системных событий засылает такое пользовательское событие в буфер, тем самым реализуется корректный выход из цикла, скажем, при клике на "крестике" окна или при выгрузке операционной системы с работающей программой.
Если в программе организованы параллельные потоки (thread), они также могут корректно взаимодействовать с основным циклом, просто посылая в очередь определенные записи.
Обработка сообщений Windows
Нередко программе не обойтись без обработки сообщений, которые система посылает консольному процессу. Значит, придется организовать цикл обработки сообщений (message loop), но ведь цикл у нас уже есть, остается только его правильно использовать.
Правда, цикл, основанный на функции WaitForSingleObjectEx, теперь не годится: эта функция реагирует только на ввод, а если его нет - цикл простаивает. Значит, надо применить альтернативу - функцию GetNumberOfConsoleInputEvents. Не забудем подключить модуль Messages. Итак, вот как может выглядеть ядро приложения, ожидающего еще и сообщений, например, от системного таймера: var
ID: Cardinal;
Mess: TMsg;
...
ID:=SetTimer(0,0,2000,nil); // установим таймер с периодом 2 сек
...
repeat
GetNumberOfConsoleInputEvents(InpHnd,Wr); // проверяем длину очереди
if Wr>0 then // не пуста: обрабатываем ввод
begin
ReadConsoleInput(InpHnd,IR,1,Wr);
case IR.EventType of
...
end;
end;
if PeekMessage(Mess,0, WM_NULL,WM_APP,PM_NOREMOVE) then // проверяем наличие сообщений
begin
GetMessage(Mess,0, WM_NULL,WM_APP); // если есть - читаем в Mess
case Mess.message of
WM_TIMER: ... // делаем что-то
end;
end;
until IR.Event.KeyEvent.wVirtualKeyCode = VK_F10;
...
KillTimer(0,ID); // удаляем таймер
Фрагмент, отвечающий за обработку сообщений, кажется избыточным, все можно было сделать проще... Но здесь учтено, что в общем случае может потребоваться обрабатывать не один, а несколько типов сообщений. Тут-то и послужит предложенная конструкция. По той же причине фильтр сообщений установлен на весь возможный их диапазон WM_NULL,WM_APP (а так - можно было бы установить: WM_TIMER,WM_TIMER).
|