SysGetKey - проблемы применения и новые решения

© Виктор Смирнов, 2015

Скачать статью в формате HTML.

Скачать архив библиотеки.

Примечание редакции:

Данная работы была представленная на заочном конкурсе работ по OS/2 «Зазеркалье 2015» и является победительницей этого конкурса.

Новая версия yaGetKey v0.07:

Домучал я её до товарного вида всё-таки.
Что нового:
- по Ctrl-Break и Ctrl-C выходит сразу, а не ждёт завершения таймаута, если время ожидания было ограничено
- на выходе по завершению таймаута может показывать реальные состояния CapsLock, NumLock, ScrollLock, Insert и какие управляющие клавиши были нажаты, если указан кортеж
- умеет выходить по изменению состояния CapsLock, NumLock, ScrollLock, Insert + по нажатию управляющих клавиш, если время ожидания ограничено и указан кортеж
- документация более-менее приведена в норму (английский уже не так коряв, но...)
- всё перетащено под Open Watcom C v1.9; теперь можно всё собрать пользуясь только его средствами.

Есть проблемы при работе под VirtualBox, но это проблемы VirtualBox-а.
Что-то они с драйвером-перехватчиком клавиатуры накрутили.

<---=================--->


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

Дело в том, что для этих языков один из символов алфавита кодируется шестнадцатиричным значением "E0"x в соответствии с применяемой кодовой страницей. Например, для русского языка и кодовой страницы CP866 - это строчная буква кириллицы "р", для болгарского и кодовой страницы CP855 - это прописная буква кириллицы "Я", для греческого и кодовой страницы CP869 - это маленькая греческая буква "зета" и так далее.

Одновременно шестнадцатиричное значение "E0"x используется в качестве индикатора получения сканкода после нажатия какой-либо клавиши расширенной функциональности, например, одной из клавиш блока управления курсором. А при последовательном побайтовом считывании данных из буфера клавиатуры, реализованном в SysGetKey(), весьма затруднительно понять является ли полученный байт со значением "E0"x первым из последовательности байтов сканкода или отдельным символом национального алфавита.

Чтобы понять, что происходит на самом деле и можно ли бороться с этим явлением, рассмотрим поведение функции SysGetKey() детально.
 

 

Описание (согласно руководствам "OS/2 Procedures Language 2/REXX" и "Object REXX Reference"):



 >>---SysGetKey(---+------------+---)---><
                   +---option---+

Считывает из буфера клавиатуры и возвращает в качестве результата очередные данные о нажатии клавиши. Если буфер клавиатуры пуст, то ожидает нажатия клавиши. В отличие от встроенной функции CHARIN(), не требует завершения процесса ввода нажатием клавиши Enter. В руководстве "OS/2 Procedures Language 2/REXX" присутствует фраза: "Выполняется аналогично функции getch() языка Си."

Параметры:

   option    (необязательный) управляет процессом эхо-вывода на экран данных о нажатии клавиши. В руководстве "OS/2 Procedures Language 2/REXX" вместо термина "данные о нажатии клавиши" (key, keystroke) используется термин "символ" (character).

Возможные значения параметра:

   "ECHO"   разрешает эхо-вывод (по умолчанию),
   "NOECHO" запрещает эхо-вывод.

Возвращаемое значение:

Данные о нажатой клавише, полученные из буфера клавиатуры.
 

Какие данные получает 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)
              <--- нажата клавиша F1
 _key="00"x   => первый символ сканкода "003B"x
 _key="3B"x   => второй символ сканкода "003B"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.
Хорошо видно, что при таком подходе невозможно отличить, например, нажатие клавиши GrayArrowUp ("E048"x) от последовательного нажатия двух клавиш - со строчной буквой кириллицы "р" ("E0"x) и с прописной буквой латиницы "H" ("48"x).

Поскольку в описании функции getch(), лежащей в основе SysGetKey(), речь идёт о чтении символов из входного потока консоли, то поиск решения, которое может помочь в данной ситуации, естественно приводит к встроенной функции CHARS().
 

Описание (согласно документации OS/2 Procedures Language 2/REXX и Object REXX Reference):



 >>---CHARS(---+----------+---)---><
               +---name---+

Возвращает в качестве результата общее количество непрочитанных символов, оставшееся в указанном входном потоке. Сюда включаются все символы-разделители, находящиеся во входном потоке. В случае статичных потоков (например, файлов) возращает общее количество символов, начиная с текущей позиции чтения до конца потока. Если имя потока не указано, то используется STDIN.

Для потоков, у которых невозможно определить общее количество непрочитанных символов (например, STDIN), в качестве результата возвращается значение 1 при наличии данных и 0, если данные отсутствуют. Для устройств OS/2 в качестве результата всегда возращается значение 1.

Параметры:

   name    (необязательный) имя входного потока (по умолчанию STDIN).

Возвращаемое значение:

Общее количество непрочитанных символов, оставшееся в указанном входном потоке.
Если количество символов в потоке определить невозможно, то возвращается 1, если данные в потоке есть, и 0, если данные отсутствуют.
Для устройств - всегда 1.
 

Логично предположить, что поскольку после нажатия клавиши расширенной функциональности требуется считать два символа из входного потока, то функция CHARS() будет сигнализировать о наличии символов во входном потоке не только после нажатия клавиши, но и после прочтения первого символа сканкода, ведь там должен остаться второй символ, согласно описаниям SysGetKey() и getch(). Однако, можно сразу сказать, что это неверно для OS/2, начиная с v2.0.

Действительно, если в MS-DOS и в OS/2 v1.x буфер клавиатуры - это очередь из одиночных байтов, то в OS/2, начиная с v2.0, в очередь ставятся элементы типа KBDKEYINFO, содержащие полную информацию о нажатой клавише, включая сканкод. Поэтому для возврата второго байта сканкода, когда это требуется, функция SysGetKey() использует данные предыдущего вызова без реального обращения ко входному потоку, поскольку элемент очереди, из которого формируется второй байт, уже был считан. Ну а CHARS() отслеживает именно элементы очереди.
 

Описание (согласно документации 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;

где:

   chChar    если бит 1 поля fbStatus сброшен - символ, соответствующий нажатой клавише в текущей раскладке клавиатуры и кодовой странице;
если бит 1 поля fbStatus установлен - 0x00 или 0xE0, информируя, что поле chScan содержит сканкод функциональной клавиши или клавиши расширенной функциональности
   chScan    сканкод нажатой клавиши
   fbStatus    флаги статуса события, вызванного нажатием клавиши
   bNlsShift    зарезервировано, должно быть равно 0
   fsState    флаги состояния переключателей и дополнительных клавиш, участвующих в комбинации нажатых клавиш
   time    отметка времени в миллисекундах

Следует признать, что без привлечения сторонних библиотек средствами Classic REXX не удастся определить чем же является только что полученный от SysGetKey() байт с шестнадцатиричным значением "E0"x - одиночным символом национального алфавита или всё-таки первым символом из двухбайтовой последовательности сканкода. А вот в Object REXX можно попытаться воспользоваться тем фактом, что чтение второго символа сканкода происходит без обращения к буферу клавиатуры и, следовательно, без задержек, а для нажатия клавиши всё таки требуется некоторое время.

Что и демонстрирует приведённый ниже пример.
 

Тест № 2:



 /* Sample2.cmd - test of SysGetGetKey() with check timeout */
 say 'Sample2 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 'Sample2 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 выключен

Использованы следующие цвета:

  • чёрный - текст, выводимый на экран во время выполнения
  • зелёный - комментарии
  • красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу

Пустые строки вставлены для удобства.

Выполнение теста № 2:


 C:\>Sample2.cmd
 Sample2 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
 Sample2 application stopped
 C:\>

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

Данная задача успешно решается с помощью функции yaGetKey() из пакета Yet Another GetKey for REXX, которая сразу возвращает двухбайтовый сканкод в тех случаях, когда это требуется.

Описание (согласно документации "Yet Another GetKey for REXX"):



 >>---yaGetKey(---+-----------------------+---)---><
                  +---time---+------------+
                             +---, stem---+ 

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

Параметры:

   time    (необязательный) неотрицательное целое число (до 9 значащих цифр).
Время ожидания нажатия клавиши в секундах. Если по истечению заданного интервала времени ни одна клавиша не была нажата, то возвращает пустую строку. Значение 0 (по умолчанию) задаёт неограниченное время ожидания.
 
   stem    (необязательный) строка, содержащая правильное корневое имя кортежа (завершающая точка может быть опущена).
Если параметр задан, то дополнительная информация о нажатой клавише помещается в элемент кортежа со следующим составным именем:

   stem.0scancode   (строка длиной 1 байт) сканкод.
Для функциональных клавиш, клавиш расширенной функциональности и комбинаций клавиш это значение соответствует второму байту двухбайтового сканкода.

Функция также заполняет указанный кортеж информацией о состоянии переключателей в момент нажатия и об использованных в комбинации управляющих клавишах, присваивая значение 0 или 1 элементам кортежа со следующими составными именами:

   stem.0sysreq_down   клавиша SysReq нажата (1) или отжата (0)
   stem.0caps_lock_down   клавиша Caps Lock нажата (1) или отжата (0)
   stem.0num_lock_down   клавиша Num Lock нажата (1) или отжата (0)
   stem.0scroll_lock_down   клавиша Scroll Lock нажата (1) или отжата (0)
   stem.0right_alt_down   правая клавиша Alt нажата (1) или отжата (0)
   stem.0right_ctrl_down   правая клавиша Ctrl нажата (1) или отжата (0)
   stem.0left_alt_down   левая клавиша Alt нажата (1) или отжата (0)
   stem.0left_ctrl_down   левая клавиша Ctrl нажата (1) или отжата (0)
   stem.0insert_on   режим вставки включен (1) или выключен (0)
   stem.0caps_lock_on   режим Caps Lock включен (1) или выключен (0)
   stem.0num_lock_on   режим Num Lock включен (1) или выключен (0)
   stem.0scroll_lock_on   режим Scroll Lock включен (1) или выключен (0)
   stem.0either_alt_down   одна или обе клавишы Alt нажаты (1) или нажатых клавиш Alt нет (0)
   stem.0either_ctrl_down   одна или обе клавишы Ctrl нажаты (1) или нажатых клавиш Ctrl нет (0)
   stem.0left_shift_down   левая клавиша Shift нажата (1) или отжата (0)
   stem.0right_shift_down   правая клавиша Shift нажата (1) или отжата (0)

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

Возвращаемое значение:

Тип и размерность возвращаемого значения зависят от обрабатываемого события:

   символ   (строка длиной 1 байт) нажата одна из символьных клавиш.
Возвращаемый символ соответствует нажатой клавише (или комбинации клавиш) в выбранной раскладке клавиатуры для текущей кодовой страницы.
   сканкод   (строка длиной 2 байт) нажата нажата функциональная клавиша, клавиша расширенной функциональности или комбинация клавиш.
   пустая строка   (строка длиной 0 байт) превышен интервал ожидания нажатия клавиши или произошла ошибка при обращении к буферу клавиатуры.

Обработка ошибок:

Специальная переменная KBDERRNO может принимать одно из следующих значений:

   ""   (пустая строка) ошибок нет
   "ERROR_KBD_FOCUS_REQUIRED"   клавиатура не в фокусе приложения
   "ERROR_KBD_KEYBOARD_BUSY"   клавиатура занята
   "ERROR_KBD_DETACHED"   приложение запущено в отключенном режиме
   "ERROR_KBD_CODE_num"   возникла ошибка с номером "num"

Функция yaGetKey() - это обёртка функции API KbdCharIn() для работы в REXX. Небольшие отличия в интерпретации результатов касаются клавиш ENTER и Slash (/) на дополнительной цифровой клавиатуре и реализованы ради большей совместимости с SysGetKey().

Приведённый ниже пример иллюстрирует работу функции yaGetKey().
 

Тест № 3:



 /* Sample3.cmd - test of yaGetKey() */
 parse version _ver
 say _ver
 if RxFuncQuery('yaGetKeyLoad') then do
    _rc = RxFuncAdd('yaGetKeyLoad', 'yaGetKey', 'yaGetKeyLoad');
    _rc = yaGetKeyLoad();
 end;
 say 'yaGetKey v.'||yaGetKeyVer()
 say 'Press Esc to quit'
 _key = '';
 do while (_key \== '1B'x)
    _key = yaGetKey();
    say '_key="'||c2x(_key)||'"x'
 end;
 _rc = yaGetKeyDrop();
 exit;

Условия выполнения теста:
  • кодовая страница консоли - CP866
  • раскладка клавиатуры - RU441
  • клавиатура переключена на латиницу
  • CapsLock выключен
Использованы следующие цвета:
  • чёрный - текст, выводимый на экран во время выполнения
  • зелёный - комментарии
  • красный - символы кириллицы, соответствующие клавишам после переключения клавиатуры на кириллицу
Пустые строки вставлены для удобства.

Выполнение теста № 3:

 C:\>Sample3.cmd
 OBJREXX 6.00 18 May 1999
 yaGetKey v.0.05
 Press Esc to quit
                <--- нажата клавиша 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)
 C:\>

Пакет Yet Another GetKey for REXX работает как в Object, так в Classic REXX на платформе OS/2. Он распространяется свободно на условиях лицензии MIT, вместе с исходными текстами, включая документацию.

В заключение требуется отметить, что аналогичная проблема использования функции SysGetKey() существует и в Open Object REXX на различных платформах. Причём особенности реализации этой функции, например, под Windows не позволяют использовать даже то половинчатое решение без применения сторонних библиотек, которое рассмотрено в данной работе (см. "Тест No 2").