Текущий раздел:

Описания RDS

Руководство программиста

Глава 2. Создание моделей блоков

§2.12. Реакция блоков на действия пользователя

§2.12.4. Реакция блоков на клавиатуру

# Руководство программиста

## Глава 2. Создание моделей блоков

### §2.12. Реакция блоков на действия пользователя

#### §2.12.4. Реакция блоков на клавиатуру

Рассматривается реакция модели блока на нажатие и отпускание клавиш. В один из рассмотренных ранее примеров добавляется возможность увеличивать и уменьшать значение выхода блока нажатием клавиш.

При необходимости, модель блока может обрабатывать события нажатия и отпускания клавиш – RDS_BFM_KEYDOWN и RDS_BFM_KEYUP соответственно. Как и в случае реакции на мышь, реакция блока на клавиатуру возможна только в режимах моделирования и расчета, в режиме редактирования информация о нажатии и отпускании клавиш в модель не передается. Для того, чтобы модель блока могла среагировать на одно из клавиатурных событий, должны одновременно выполниться два условия:

- окно подсистемы, в которой находится блок, должно иметь фокус ввода (быть самым верхним окном, при условии, что RDS – активное приложение);
- в параметрах блока должна быть разрешена реакция на клавиатуру.

В RDS нет понятия «текущего блока» или «блока, имеющего фокус ввода». При нажатии или отпускании какой-либо клавиши поочередно вызываются модели всех блоков в активном окне подсистемы, для которых разрешена реакция на клавиатуру. Порядок вызова моделей определяется внутренней логикой RDS, и ни разработчик, ни пользователь не могут на него повлиять. Единственное, что может сделать модель блока – это, среагировав на интересующее ее сочетание клавиш, сообщить RDS о том, что событие уже обработано, и дальнейший вызов моделей не требуется. При этом RDS не только прекращает перебирать и вызывать блоки, которым разрешена реакция на клавиатуру, но и не выполняет пункт главного меню, которому соответствует нажатая комбинация клавиш (если, конечно, такой имеется). Например, пункту главного меню RDS меню «расчет | стоп» соответствует клавиша F7, поэтому нажатие этой клавиши обычно останавливает запущенный расчет. Однако, если в активном в данный момент окне подсистемы будет находиться блок, модель которого среагирует на нажатие F7 и сообщит RDS, что событие обработано, расчет остановлен не будет. Таким образом, обработка клавиатуры моделями блоков имеет приоритет над стандартными реакциями RDS. Тем не менее, если в окне подсистемы будет два блока, обрабатывающих нажатие одной и той же клавиши и сообщающих RDS об успешной обработке события, нельзя заранее сказать, модель которого из блоков будет выполнена.

Реакция на нажатие и отпускание клавиш используется в моделях блоков реже, чем реакция на мышь. Как правило, такие реакции используются для включения и выключения различных кнопок, подачи команд и т.п. На клавиатуру могут реагировать все блоки активной подсистемы, независимо от видимости и активности слоя, на котором они расположены, что позволяет скрыть от пользователя логику обработки клавиатуры в подсистеме. Однако, следует помнить, что при закрытии окна подсистемы или уходе его с переднего плана блоки этой подсистемы перестают реагировать на клавиатуру, поэтому для вызова каких-либо общесистемных функций такие реакции непригодны. Если разработчику необходимо сделать так, чтобы модель блока реагировала на нажатие какой-либо комбинации клавиш независимо от того, открыто ли окно подсистемы с этим блоком и имеет ли это окно фокус, он может добавить в модель регистрацию дополнительного пункта системного меню и связать с этим пунктом «горячую клавишу». При этом нажатие этой клавиши будет вызывать модель блока для реакции на добавленный пункт меню. Механизм регистрации пунктов системного меню и реакции на них описан в §2.12.7.

В §2.12.1 описан блок, увеличивающий и уменьшающий значение своего выхода при щелчках левой кнопкой мыши на его изображении. Добавим в него похожую реакцию на клавиатуру: нажатие одной клавиши (или сочетания клавиш) будет увеличивать значение выхода на единицу, нажатие другой – уменьшать. Причем клавиши, на которые реагирует блок, мы сделаем настраиваемыми.

Чтобы было где хранить коды клавиш, на которые реагирует блок, создадим для него личную область данных, как всегда оформив ее в виде класса:

      //====== Класс личной области данных ======
      class TPlusMinusData
      { public:
          int KeyPlus;       // Клавиша увеличения
          DWORD ShiftsPlus;  // и ее флаги
          int KeyMinus;      // Клавиша уменьшения
          DWORD ShiftsMinus; // и ее флаги

          int Setup(void);    // Функция настройки клавиш
          void SaveBin(void); // Сохранение параметров
          int LoadBin(void);  // Загрузка параметров

          // Конструктор класса
          TPlusMinusData(void)
            { KeyPlus=ShiftsPlus=KeyMinus=ShiftsMinus=0;};
      };
      //=========================================

В классе содержатся два поля для клавиши увеличения значения (KeyPlus, ShiftsPlus) и два поля для клавиши уменьшения (KeyMinus, ShiftsMinus). Каждое сочетание клавиш, на которые будет реагировать блок, задается двумя целыми числами: первое (KeyPlus, KeyMinus) содержит код клавиши, второе (ShiftsPlus, ShiftsMinus) – флаги, указывающие на состояние клавиш Ctrl, Alt и Shift. Например, для того, чтобы блок увеличил значение выхода при нажатии сочетания клавиш Ctrl + Alt + F1, в поле KeyPlus необходимо записать стандартную константу API Windows VK_F1, а в ShiftsPlus – объединенные битовым «ИЛИ» флаги RDS_KCTRL и RDS_KALT, описанные в «RdsDef.h». Нулевое значение поля флагов указывает на то, что блок должен реагировать на нажатие клавиши без Ctrl, Alt и Shift. Нулевое значение кода клавиши не соответствует ни одной клавише клавиатуры, поэтому может использоваться как признак отсутствия реакции: оно никогда не совпадет с каким-либо кодом.

Рис. 79. Задание сочетаний клавиш

Для того, чтобы пользователь мог задать сочетания клавиш, на которые должен реагировать блок, в класс включена функция настройки Setup. Вводить числовые коды клавиш и флагов неудобно, поэтому в этой функции мы будем пользоваться специальными полями ввода для задания клавиш: при попадании в них курсора пользователь может просто нажать нужное сочетание клавиш, и оно отобразится в поле ввода в понятном ему виде. Например, на рис. 79 для увеличения и уменьшения значения выхода блока заданы сочетания клавиш Ctrl + цифр.8 и Ctrl + цифр.2 соответственно (клавиши «8» и «2», в данном случае, взяты с цифрового блока клавиатуры, где они соответствуют курсорным стрелкам вверх и вниз). Нажатие клавиши Backspace в таком поле ввода приведет к обнулению кода клавиши и, таким образом, к отмене реакции (в поле при этом будет написано слово «нет»).

С использованием полей для ввода кодов клавиш функция Setup будет иметь следующий вид:

      // Функция задания клавиш
      // ВАЖНО: Исходный текст программы должен быть записан в UTF8,
      //   в противном случае необходимо использовать версии функций
      //   с суффиксом "W" и символьные константы с префиксом "L"
      int TPlusMinusData::Setup(void)
      { RDS_HOBJECT window; // Идентификатор вспомогательного объекта
        BOOL ok;            // Пользователь нажал "OK"
        // Создание окна
        window=rdsFORMCreate(FALSE,-1,-1,"Плюс/минус");
        // Добавление полей ввода
        rdsFORMAddEdit(window,0,1,RDS_FORMCTRL_HOTKEY,
                       "Клавиша увеличения:",80);
        rdsFORMAddEdit(window,0,2,RDS_FORMCTRL_HOTKEY,
                       "Клавиша уменьшения:",80);
        // Занесение исходных значений кодов клавиш в поля ввода
        rdsSetObjectInt(window,1,RDS_FORMVAL_VALUE,KeyPlus);
        rdsSetObjectInt(window,1,RDS_FORMVAL_HKSHIFTS,ShiftsPlus);
        rdsSetObjectInt(window,2,RDS_FORMVAL_VALUE,KeyMinus);
        rdsSetObjectInt(window,2,RDS_FORMVAL_HKSHIFTS,ShiftsMinus);
        // Открытие окна
        ok=rdsFORMShowModalEx(window,NULL);
        if(ok)
          { // Нажата кнопка OK - запись кодов клавиш в класс
            KeyPlus=rdsGetObjectInt(window,1,RDS_FORMVAL_VALUE);
            ShiftsPlus=rdsGetObjectInt(window,1,RDS_FORMVAL_HKSHIFTS);
            KeyMinus=rdsGetObjectInt(window,2,RDS_FORMVAL_VALUE);
            ShiftsMinus=rdsGetObjectInt(window,2,RDS_FORMVAL_HKSHIFTS);
          }
        // Уничтожение окна
        rdsDeleteObject(window);
        // Возвращаемое значение
        return ok?RDS_BFR_MODIFIED:RDS_BFR_DONE;
      }
      //=========================================

Для задания кодов клавиш используется новый тип поля ввода RDS_FORMCTRL_HOTKEY. Поскольку каждому сочетанию клавиш соответствует два целых числа (код клавиши и флаги), данные в это поле ввода заносятся двумя вызовами функции rdsSetObjectInt: с обычной константой RDS_FORMVAL_VALUE заносится код клавиши, а с константой RDS_FORMVAL_HKSHIFTS – флаги. Чтение данных также производится двумя вызовами rdsGetObjectInt с теми же константами. В остальном эта функция ничем не отличается от других функций настройки, уже рассматривавшихся ранее.

Для сохранения и загрузки заданных пользователем клавиш (будет не очень хорошо, если ему придется каждый раз после загрузки схемы настраивать клавиши заново) будем использовать функции SaveBin и LoadBin соответственно. Как видно из их названий, на сей раз мы будем сохранять и загружать данные не в текстовом, а в двоичном формате. Недостатки двоичного формата описаны в §2.8.1, но для данного простого блока его вполне можно использовать. Для того чтобы сделать формат более гибким и оставить возможность, при необходимости, легко изменить или дополнить его, будем использовать «теговую» запись: перед каждой группой данных (в данном случае – перед парой целых чисел, описывающих сочетание клавиш) будем записывать байт, значение которого будет указывать на назначение следующих за ним данных. В конце запишем нулевой байт, который будет означать конец данных. Функция сохранения параметров будет такой:

      // Сохранение параметров
      void TPlusMinusData::SaveBin(void)
      { BYTE tag; // Переменная для байта тега

        tag=1; // Тег 1 - клавиша увеличения
        rdsWriteBlockData(&tag,sizeof(tag));
        rdsWriteBlockData(&KeyPlus,sizeof(KeyPlus));
        rdsWriteBlockData(&ShiftsPlus,sizeof(ShiftsPlus));

        tag=2; // Тег 2 - клавиша уменьшения
        rdsWriteBlockData(&tag,sizeof(tag));
        rdsWriteBlockData(&KeyMinus,sizeof(KeyMinus));
        rdsWriteBlockData(&ShiftsMinus,sizeof(ShiftsMinus));

        tag=0; // Тег 0 - конец данных
        rdsWriteBlockData(&tag,sizeof(tag));
      }
      //=========================================

Если в будущем потребуется добавить в этот блок новые параметры, или изменить формат хранения кодов клавиш, нужно будет записывать новые группы данных с новыми тегами (3, 4, 5 и т.д.). Такая запись позволяет не терять совместимости со старым форматом.

Напишем теперь функцию загрузки для этого формата:

      // Загрузка параметров
      int TPlusMinusData::LoadBin(void)
      { BYTE tag;

        for(;;) // Цикл до тех пор, пока данные не кончатся
          { // Читаем байт тега
            if(!rdsReadBlockData(&tag,sizeof(tag)))
              break; // Тег не считан - данные кончились
            // Анализируем считанный тег
            switch(tag)
              { case 0: // Конец данных блока
                  return RDS_BFR_DONE; // Загрузка успешно завершена
                case 1: // Данные клавиши увеличения
                  rdsReadBlockData(&KeyPlus,sizeof(KeyPlus));
                  rdsReadBlockData(&ShiftsPlus,sizeof(ShiftsPlus));
                  break;
                case 2: // Данные клавиши уменьшения
                  rdsReadBlockData(&KeyMinus,sizeof(KeyMinus));
                  rdsReadBlockData(&ShiftsMinus,sizeof(ShiftsMinus));
                  break;
                default: // Неопознанный тег
                  return RDS_BFR_ERROR; // Сообщаем RDS об ошибке
              }
          }
        // Данные кончились до тега 0 - сообщаем об ошибке
        return RDS_BFR_ERROR;
      }
      //=========================================

Внутри этой функции находится цикл for(;;) без явного условия завершения, в теле которого первым делом из данных блока читается один байт – это должен быть байт тега. Если байт считать не удалось, значит, данные блока неожиданно закончились, и цикл завершается оператором break (при этом функция возвращает RDS_BFR_ERROR, сообщая RDS об ошибке). В противном случае считанный байт анализируется оператором switch, и, если его значение – 1 или 2, из данных блока читаются два целых поля класса, описывающие клавишу увеличения или уменьшения соответственно. Если считан тег 0, значит, на этом данные блока завершаются – функция возвращает RDS_BFR_DONE. Если же считанный байт имеет какое-либо другое значение, функция вернет RDS_BFR_ERROR – тег не опознан, в формате записи, реализованном в функции SaveBin он не предусмотрен, и загрузка данных невозможна.

Если в будущем мы добавим новые данные и соответствующие им новые теги в функцию SaveBin, в функцию LoadBin нужно будет просто добавить новые метки case и команды загрузки новых данных после них.

Описывая данный формат записи, следует сделать одно, довольно очевидное, замечание. В качестве тега мы здесь используем один байт, поэтому всего может быть 256 различных тегов. Что же делать, если, в процессе модернизации блока, нам потребуется записывать больше различных групп данных? В этом случае одно какое-либо значение тега (например, 255) нужно использовать в качестве префикса, за которым будет следовать новый тег. Таким образом, первые 255 параметров (включая тег конца данных) будут иметь однобайтовые теги 0…254, а следующие за ними – двухбайтовые: (255,0), (255,1), и т.д. Впрочем, в данном блоке нам вряд ли когда-либо понадобятся более 256 параметров.

Теперь, когда класс личной области данных блока и его функции описаны, внесем соответствующие изменения в функцию модели:

      // Увеличение/уменьшение значения по щелчку и клавишам
      extern "C" __declspec(dllexport)
        int RDSCALL PlusMinus(int CallMode,
                          RDS_PBLOCKDATA BlockData,
                          LPVOID ExtParam)
      {
      // Макроопределения для статических переменных
      #define pStart ((char *)(BlockData->VarTreeData))
      #define Start (*((char *)(pStart)))
      #define Ready (*((char *)(pStart+RDS_VSZ_S)))
      #define v (*((RDSINT32 *)(pStart+2*RDS_VSZ_S)))
        // Вспомогательная — указатель на структуру события мыши
        RDS_PMOUSEDATA mouse;
        // Вспомогательная — указатель на структуру события клавиатуры
        RDS_PKEYDATA key;
        // Указатель на личную область данных блока,приведенный к
        // правильному типу
        TPlusMinusData *data=(TPlusMinusData*)(BlockData->BlockData);
        switch(CallMode)
          { // Проверка типа статических переменных
            case RDS_BFM_VARCHECK:
              return strcmp((char*)ExtParam,"{SSI}")?
                RDS_BFR_BADVARSMSG:RDS_BFR_DONE;

            // Реакция на нажатие кнопки мыши
            case RDS_BFM_MOUSEDOWN:
              // Приведение ExtParam к нужному типу
              mouse=(RDS_PMOUSEDATA)ExtParam;
              if(mouse->Button==RDS_MLEFTBUTTON)
                { // Нажата левая кнопка
                  // Проверяем, есть ли у блока картинка (получаем описание блока)
                  RDS_BLOCKDESCRIPTION descr;
                  descr.servSize=sizeof(descr);
                  rdsGetBlockDescription(BlockData->Block,&descr);
                  if(descr.Flags & RDS_BDF_HASPICTURE)
                    { // Картинка есть – определяем идентификатор элемента под курсором
                      int id=rdsGetMouseObjectId(mouse);
                      v+=id;
                    }
                  else if(mouse->y<mouse->Top+mouse->Height/2)
                    v++; // В верхней половине блока - увеличиваем
                  else
                    v--; // В нижней половине блока — уменьшаем
                  // Взводим сигнал готовности
                  Ready=1;
                }
              break;

            case RDS_BFM_INIT: // Инициализация
              BlockData->BlockData=new TPlusMinusData();
              break;
            case RDS_BFM_CLEANUP: // Очистка данных
              delete data;
              break;
            case RDS_BFM_SETUP: // Настройка параметров
              return data->Setup();
            case RDS_BFM_SAVEBIN: // Сохранение параметров
              data->SaveBin();
              break;
            case RDS_BFM_LOADBIN: // Загрузка параметров
              return data->LoadBin();

            // Реакция на нажатие клавиши
            case RDS_BFM_KEYDOWN:
              // Приведение ExtParam к нужному типу
              key=(RDS_PKEYDATA)ExtParam;
              // Сравнение нажатой клавиши с клавишами уменьшения и увеличения
              if(key->KeyCode==data->KeyPlus &&
                 key->Shift==data->ShiftsPlus)
                { v++; Ready=1; }
              else if(key->KeyCode==data->KeyMinus &&
                      key->Shift==data->ShiftsMinus)
                { v--; Ready=1; }
              break;
          }
        return RDS_BFR_DONE;
      // Отмена макроопределений
      #undef v
      #undef Ready
      #undef Start
      #undef pStart
      }
      //=========================================

Добавленные в модель реакции на события инициализации (RDS_BFM_INIT), очистки данных (RDS_BFM_CLEANUP), а также настройки (RDS_BFM_SETUP), загрузки (RDS_BFM_SAVEBIN) и сохранения (RDS_BFM_LOADBIN) параметров стандартны и уже неоднократно описывались ранее. Рассмотрим подробнее реакцию модели на нажатие клавиши: в этом случае она вызывается в режиме RDS_BFM_KEYDOWN.

При каждом вызове модели для реакции на нажатие или отпускание клавиши ей передается указатель на структуру RDS_KEYDATA, описывающую произошедшее событие:

      typedef struct
      { int KeyCode;      // Код клавиши (Windows API)
        BOOL Repeat;      // При нажатии: нажатие из-за автоповтора
        int  RepeatCount; // При Repeat==TRUE - число повторенных
                          // с прошлого вызова
        DWORD Shift;      // Флаги клавиатуры (RDS_M*, RDS_K*)
        int KeyEvent;     // Причина вызова - RDS_BFM_KEYDOWN или
                          // RDS_BFM_KEYUP
        BOOL Handled;     // Возвращаемый параметр - событие обработано
      } RDS_KEYDATA;
      typedef RDS_KEYDATA *RDS_PKEYDATA;

Нас будут интересовать поля KeyCode и Shift, содержащие код нажатой клавиши и флаги состояния Ctrl-Shift-Alt соответственно. Они сравниваются с кодами клавиш и флагами в параметрах блока, и, при совпадении с одной из пар чисел, значение выхода блока увеличивается или уменьшается на 1, после чего взводится сигнал готовности Ready, чтобы новое значение передалось по связям.

Если бы мы хотели сообщить RDS, что нажатие клавиши обработано, и дальнейшие реакции не требуются, мы могли бы присвоить полю Handled структуры RDS_KEYDATA значение TRUE, либо вернуть из функции модели не константу RDS_BFR_DONE, как обычно, а RDS_BFR_STOP (или любое другое ненулевое значение). Но нам этого не требуется, наоборот, для данной модели лучше, чтобы обработка клавиатуры продолжалась и после реакции. В этом случае несколько блоков, настроенные на одну и ту же клавишу, одновременно изменят значения своих выходов при ее нажатии.

Для проверки работы измененной модели необходимо разрешить в параметрах всех блоков, к которым она подключена, реакцию на клавиатуру и вызов функции настройки (рис. 80). Затем необходимо в настройках этих блоков задать клавиши для увеличения и уменьшения значений (см. рис. 79). К выходам блоков следует подключить какие-либо индикаторы. Теперь, если расчет запущен, и окно подсистемы, в которой находятся эти блоки, находится на переднем плане (имеет фокус), нажатие на клавиши, указанные в настройках блоков, будет приводить к изменению значений на подключенных к этим блокам индикаторах. Причем если несколько блоков настроены на использование одной и той же клавиши, их значения будут изменяться одновременно. Нужно еще раз подчеркнуть, что все это будет происходить только в том случае, если окно подсистемы будет на переднем плане. Если самым верхним будет другое окно (не обязательно окно другой подсистемы – это может быть главное окно RDS, окно редактора слоев и т.п.), то блоки не будут реагировать на клавиатуру.

Рис. 80. Включение реакции на клавиатуру в параметрах блока