Особенности реализации SysGetKey() в REXX/Object REXX под OS/2
Автор: Виктор Смирнов
Дата: 22.05.2015
Источник: vasm.livejournal.com
Одновременно шестнадцатиричное значение "E0"x используется в качестве индикатора получения сканкода после нажатия какой-либо клавиши расширенной функциональности, например, одной из клавиш блока управления курсором. А при последовательном побайтовом считывании данных из буфера клавиатуры, реализованном в SysGetKey(), весьма затруднительно понять является ли полученный байт со значением "E0"x первым из последовательности байтов сканкода или отдельным символом национального алфавита.
Чтобы понять, что происходит на самом деле и можно ли бороться с этим явлением, протребовалось провести детальное исследование поведения функции SysGetKey().
Поведение, ожидаемое от SysGetKey(), можно узнать из документации.
Описание (согласно руководствам "OS/2 Procedures Language 2/REXX" и "Object REXX Reference"):
>>---SysGetKey(---+------------+---)--->< +---option---+ |
||||||
Считывает из буфера клавиатуры и возвращает в качестве результата очередные данные о нажатии клавиши. Если буфер клавиатуры пуст, то ожидает нажатия клавиши. В отличие от встроенной функции CHARIN(), не требует завершения процесса ввода нажатием клавиши Enter. В руководстве "OS/2 Procedures Language 2/REXX" присутствует фраза: "Выполняется аналогично функции getch() языка Си." Параметры:
Возвращаемое значение: Данные о нажатой клавише, полученные из буфера клавиатуры. |
Какие данные получает SysGetKey() из буфера клавиатуры в документации не расшифровывается. Подсказка об аналогии с функцией getch() языка Си в руководстве "OS/2 Procedures Language 2/REXX" позволяет немного прояснить ситуацию: "Функция getch() считывает символ из входного потока консоли. При чтении данных о нажатии функциональных клавиш или клавиш расширенной функциональности каждая функция должна вызываться дважды; первый вызов возвращает 0x00 или 0xE0, а второй вызов возвращает действительный код клавиши."
Исходя из этого, можно ожидать, что SysGetKey() всегда возвращает однобайтовую строку, содержащую либо символ, соответствующий нажатой клавише, либо один из байтов сканкода клавиши. Сканкод идентифицируется получением байта со значением "00"x или "E0"x.
Приведённый ниже тест позволяет убедиться в этом.
Тест № 1:
/* Sample1.cmd - test of SysGetKey() */ parse version _ver say _ver if RxFuncQuery('SysLoadFuncs') then do _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'); _rc = SysLoadFuncs(); end; say SysVersion() say 'RexxUtil v.'||SysUtilVersion() say 'Press Esc to quit' _key = ''; do while (_key \== '1B'x) _key = SysGetKey('NOECHO'); say '_key="'||c2x(_key)||'"x' end; _rc = SysDropFuncs(); exit; |
Условия выполнения теста:
- кодовая страница консоли - CP866
- раскладка клавиатуры - RU441
- клавиатура переключена на латиницу
- CapsLock выключен
- чёрный - текст, выводимый на экран во время выполнения
- зелёный - комментарии
- красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Выполнение теста № 1:
C:\>Sample1.cmd OBJREXX 6.00 18 May 1999 OS/2 2.45 RexxUtil v.2.00 Press Esc to quit <--- нажата клавиша 2 _key="32"x => символ "2" (DIGIT TWO) <--- нажата комбинация клавиш Shift+2 _key="40"x => символ "@" (COMMERCIAL AT) <--- нажата комбинация клавиш Ctrl+2 _key="00"x => первый символ сканкода "0003"x _key="03"x => второй символ сканкода "0003"x <--- нажата комбинация клавиш Alt+2 _key="00"x => первый символ сканкода "0079"x _key="79"x => второй символ сканкода "0079"x <--- нажата клавиша F1 _key="00"x => первый символ сканкода "003B"x _key="3B"x => второй символ сканкода "003B"x <--- нажата комбинация клавиш Shift+F1 _key="00"x => первый символ сканкода "0054"x _key="54"x => второй символ сканкода "0054"x <--- нажата комбинация клавиш Ctrl+F1 _key="00"x => первый символ сканкода "005E"x _key="5E"x => второй символ сканкода "005E"x <--- нажата комбинация клавиш Alt+F1 _key="00"x => первый символ сканкода "0068"x _key="68"x => второй символ сканкода "0068"x <--- нажата клавиша GrayArrowUp _key="E0"x => первый символ сканкода "E048"x _key="48"x => второй символ сканкода "E048"x <--- нажата комбинация клавиш Shift+GrayArrowUp _key="E0"x => первый символ сканкода "E048"x _key="48"x => второй символ сканкода "E048"x <--- нажата комбинация клавиш Ctrl+GrayArrowUp _key="E0"x => первый символ сканкода "E08D"x _key="8D"x => второй символ сканкода "E08D"x <--- нажата комбинация клавиш Alt+GrayArrowUp _key="00"x => первый символ сканкода "0098"x _key="98"x => второй символ сканкода "0098"x <--- нажата клавиша h _key="68"x => символ "h" (LATIN SMALL LETTER H) <--- нажата комбинация клавиш Shift+h _key="48"x => символ "H" (LATIN CAPITAL LETTER H) <--- нажата комбинация клавиш Ctrl+h _key="08"x => управляюший символ BS (BACKSPACE) <--- нажата комбинация клавиш Alt+h _key="00"x => первый символ сканкода "0023"x _key="23"x => второй символ сканкода "0023"x <--- переключаем клавиатуру на кириллицу <--- нажата клавиша р _key="E0"x => символ "р" (CYRILLIC SMALL LETTER ER) <--- нажата комбинация клавиш Shift+р _key="90"x => символ "Р" (CYRILLIC CAPITAL LETTER ER) <--- нажата комбинация клавиш Ctrl+р _key="08"x => управляюший символ BS (BACKSPACE) <--- нажата комбинация клавиш Alt+р _key="00"x => первый символ сканкода "0023"x _key="23"x => второй символ сканкода "0023"x <--- нажата клавиша GrayArrowUp _key="E0"x => первый символ сканкода "E048"x _key="48"x => второй символ сканкода "E048"x <--- нажата клавиша р _key="E0"x => символ "р" (CYRILLIC SMALL LETTER ER) <--- переключаем клавиатуру на латиницу <--- нажата комбинация клавиш Shift+h _key="48"x => символ "H" (LATIN CAPITAL LETTER H) <--- нажата клавиша Enter _key="0D"x => управляюший символ CR (CARRIAGE RETURN) <--- нажата клавиша Esc _key="1B"x => управляюший символ ESC (ESCAPE) C:\> |
Результаты теста однозначно демонстрируют соответствие реализации ожиданиям:
- возврат значений из функции SysGetKey() происходит при КАЖДОМ нажатии на значащую клавишу (или комбинацию клавиш),
- возвращаемые символы соответствуют выбранной раскладке клавиатуры и кодовой странице,
- возвращаемые сканкоды соответствуют функциональным клавишам, клавишам расширенной функциональности и комбинациям клавиш,
- нажатие комбинации клавиш Ctrl+C ожидаемо прерывает выполнение, вызывая обработку условия HALT.
Поскольку в описании функции getch(), лежащей в основе SysGetKey(), речь идёт о чтении символов из входного потока консоли, то поиск решения, которое может помочь в данной ситуации, естественно приводит к встроенной функции CHARS().
Описание (согласно документации OS/2 Procedures Language 2/REXX и Object REXX Reference):
>>---CHARS(---+----------+---)--->< +---name---+ |
||
Возвращает в качестве результата общее количество непрочитанных символов, оставшееся в указанном входном потоке. Сюда включаются все символы-разделители, находящиеся во входном потоке. В случае статичных потоков (например, файлов) возращает общее количество символов, начиная с текущей позиции чтения до конца потока. Если имя потока не указано, то используется STDIN. Для потоков, у которых невозможно определить общее количество непрочитанных символов (например, STDIN), в качестве результата возвращается значение 1 при наличии данных и 0, если данные отсутствуют. Для устройств OS/2 в качестве результата всегда возращается значение 1. Параметры:
Возвращаемое значение: Общее количество непрочитанных символов, оставшееся в указанном входном потоке. Если количество символов в потоке определить невозможно, то возвращается 1, если данные в потоке есть, и 0, если данные отсутствуют. Для устройств - всегда 1. |
Исходя из данного описания, логично предположить, что поскольку после нажатия клавиши расширенной функциональности требуется считать два символа из входного потока, то функция CHARS() будет сигнализировать о наличии символов во входном потоке не только после нажатия клавиши, но и после прочтения первого символа сканкода.
Приведённый ниже тест позволяет проверить это.
Тест № 2:
/* Sample2.cmd - test of SysGetKey() with Chars() */ parse version _ver say _ver if RxFuncQuery('SysLoadFuncs') then do _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'); _rc = SysLoadFuncs(); end; say SysVersion() say 'RexxUtil v.'||SysUtilVersion() say 'Press Esc to quit' _delay = 0.01; _sleep = 0; _key = ''; do while (_key \== '1B'x) _rc = Chars(); if (_rc > 0) then do _key = SysGetKey('NOECHO'); say 'Chars()='||_rc||' _key="'||c2x(_key)||'"x' _sleep = 0; end; else do if (\_sleep) then do say 'Chars()='||_rc||' SysSleep()' say '' _sleep = 1; end; _rc = SysSleep(_delay); end; end; _rc = SysDropFuncs(); exit; |
Условия выполнения теста:
- кодовая страница консоли - CP866
- раскладка клавиатуры - RU441
- клавиатура переключена на латиницу
- CapsLock выключен
Использованы следующие цвета:
- чёрный - текст, выводимый на экран во время выполнения
- зелёный - комментарии
- красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.
Выполнение теста № 2:
C:\>Sample2.cmd OBJREXX 6.00 18 May 1999 OS/2 2.45 RexxUtil v.2.00 Press Esc to quit Chars()=0 SysSleep() <--- нажата клавиша 2 Chars()=1 _key="32"x => символ "2" (DIGIT TWO) Chars()=0 SysSleep() <--- нажата клавиша F1 Chars()=1 _key="00"x => первый символ сканкода "003B"x Chars()=0 SysSleep() <--- нажата клавиша Enter Chars()=1 _key="3B"x => второй символ сканкода "003B"x Chars()=1 _key="0D"x => управляюший символ CR (CARRIAGE RETURN) Chars()=0 SysSleep() <--- нажата клавиша GrArrowUp Chars()=1 _key="E0"x => первый символ сканкода "E048"x Chars()=0 SysSleep() <--- нажата клавиша Enter Chars()=1 _key="48"x => второй символ сканкода "E048"x Chars()=1 _key="0D"x => управляюший символ CR (CARRIAGE RETURN) Chars()=0 SysSleep() <--- переключаем клавиатуру на кириллицу <--- нажата клавиша р Chars()=1 _key="E0"x => символ "р" (CYRILLIC SMALL LETTER ER) Chars()=0 SysSleep() <--- переключаем клавиатуру на латиницу <--- нажата комбинация клавиш Shift+h Chars()=1 _key="48"x => символ "H" (LATIN CAPITAL LETTER H) Chars()=0 SysSleep() <--- нажата клавиша Esc Chars()=1 _key="1B"x => управляюший символ ESC (ESCAPE) C:\> |
Очевидно, что результаты теста отличаются от ожидаемых. Функция CHARS() отмечает появление данных во входном потоке сразу после нажатия клавиши. А последующий вызов SysGetKey() очищает эти данные независимо того, что было считано - одиночный символ, соответствующий клавише, или первый символ сканкода. Второй символ сканкода может быть считан даже в том случае, если CHARS() сигнализирует об отсутствии данных во входном потоке, и его считывание не изменяет состояние входного потока. Появление данных во входном потоке регистрируется только после следующего нажатия клавиши.
А такое поведение приводит к выводу, что при считывании второго символа сканкода используются данные предыдущего вызова функции SysGetKey() без реального обращения ко входному потоку. И что входной поток, связанный с клавиатурным вводом, представляет собой не просто последовательность одиночных байтов, а очередь элементов, содержащих полную информацию о нажатой клавише.
Возникшая терминологическая путаница в описаниях, вызвавшая ложные ожидания, связана как с историческими причинами, так и с многоплатформенностью языка Си. Действительно, если в MS-DOS и в OS/2 v1.x буфер клавиатуры - это очередь из одиночных байтов, то в OS/2, начиная с v2.0, в очередь ставятся элементы типа KBDKEYINFO. И если для MS-DOS и OS/2 v1.x ожидаемое поведение полностью соответствует описаниям, то для понимания того, что следует ожидать в OS/2 v2.0 и выше информации в документации REXX явно недостаточно.
Описание (согласно документации Control Programming Guide and Reference из пакета OS/2 ToolKit):
typedef struct _KBDKEYINFO { UCHAR chChar; UCHAR chScan; UCHAR fbStatus; UCHAR bNlsShift; USHORT fsState; ULONG time; } KBDKEYINFO; typedef KBDKEYINFO *PKBDKEYINFO; |
||||||||||||
где:
|
Исходя из вышеизложенного, следует признать, что для Classic REXX поставленная задача не имеет решения без привлечения сторонних библиотек. А вот в Object REXX можно попытаться воспользоваться тем фактом, что чтение второго символа сканкода происходит без обращения к буферу клавиатуры и, следовательно, без задержек, а для нажатия клавиши всё таки требуется некоторое время.
Что и демонстрирует приведённый ниже пример.
Тест № 3:
/* Sample3.cmd - test of SysGetGetKey() with check timeout */ say 'Sample3 application started' parse version _ver say _ver if RxFuncQuery('SysLoadFuncs') then do _rc = RxFuncAdd('SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'); _rc = SysLoadFuncs(); end; say SysVersion() say 'RexxUtil v.'||SysUtilVersion() say 'Press Esc to quit' signal on halt _kbd = .ownKbd~new; _kbd~run; _wait = 0.05; _delay = 0.01; _key = ''; do while (_key \== '1B'x) _key = SysGetKey('NOECHO'); if (_key == '00'x) then do _key = _key||SysGetKey('NOECHO'); end; _kbd~keys~queue(_key); if (_key \== 'E0'x) then do _rc = SysSleep(_delay); end; end; halt: _kbd~stop = .true; _rc = SysSleep(_wait); drop _kbd; say 'Sample3 application stopped' _rc = SysDropFuncs(); exit; ::class ownKbd ::method stop attribute unguarded ::method keys attribute unguarded ::method init self~stop = .false; self~keys = .queue~new; say 'Instance of '||self~defaultname||' created' return; ::method uninit self~stop = .true; say 'Instance of '||self~defaultname||' destroyed' return; ::method run say 'Run() method of '||self~defaultname||' processed' reply _delay = 0.001; _key = ''; do while (\self~stop) if (self~keys~items > 0) then do _key = self~keys~pull; if ((_key == 'E0'x) & (self~keys~items > 0)) then do _key = _key||self~keys~pull; end; say '_key="'||_key~c2x||'"x' end; _rc = SysSleep(_delay); end; say 'Run() method of '||self~defaultname||' terminated' return; |
Условия выполнения теста:
- кодовая страница консоли - CP866
- раскладка клавиатуры - RU441
- клавиатура переключена на латиницу
- CapsLock выключен
Использованы следующие цвета:
- чёрный - текст, выводимый на экран во время выполнения
- зелёный - комментарии
- красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.
Выполнение теста № 3:
C:\>Sample3.cmd Sample3 application started OBJREXX 6.00 18 May 1999 OS/2 2.45 RexxUtil v.2.00 Press Esc to quit Instance of an OWNKBD created Run() method of an OWNKBD processed <--- нажата клавиша 2 _key="32"x => символ "2" (DIGIT TWO) <--- нажата клавиша F1 _key="003B"x => сканкод "003B"x <--- нажата клавиша GrArrowUp _key="E048"x => сканкод "E048"x <--- переключаем клавиатуру на кириллицу <--- нажата клавиша р _key="E0"x => символ "р" (CYRILLIC SMALL LETTER ER) <--- переключаем клавиатуру на латиницу <--- нажата комбинация клавиш Shift+h _key="48"x => символ "H" (LATIN CAPITAL LETTER H) <--- нажата клавиша Enter _key="0D"x => управляюший символ CR (CARRIAGE RETURN) <--- нажата клавиша Esc _key="1B"x => управляюший символ ESC (ESCAPE) Run() method of an OWNKBD terminated Instance of an OWNKBD destroyed Sample3 application stopped C:\> |
Этот способ, конечно, не идеален. Возможны ошибки распознавания, например, при высокой скорости набора или для сильно нагруженных приложений. Таким образом потребность в функции, которая позволяла бы уверенно отличать нажатия клавиш расширенной функциональности от нажатия указанной клавиши национального алфавита, всё равно остаётся.