События клавиатуры

MQL-программы могут получать от терминала сообщения о нажатиях клавиш на клавиатуре, обрабатывая в функции OnChartEvent события CHARTEVENT_KEYDOWN.

Важно отметить, что события генерируются только в активном графике, и только в том случае, когда он имеет фокус ввода.

В системе Windows фокусом называется логическое и визуальное выделение одного конкретного окна, с которым в данный момент взаимодействует пользователь. Как правило, перемещение фокуса осуществляется кликом мыши или специальными сочетаниями клавиш (Tab, Ctrl+Tab), в результате чего выбранное окно тем или иными образом подсвечивается. Например, в поле ввода появится текстовый курсор, в списке раскрашивается текущая строка альтернативным цветом и т.д.

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

  • Alt+W вызывает окно со списком графиков, в котором можно выбрать один;
  • Ctrl+F6 выполняет переключение на следующий график (в списке окон, где порядок соответствует, как правило, порядку закладок);
  • Crtl+Shift+F6 выполняет переключение на предыдущий график;

С полным перечнем горячих клавиш MetaTrader 5 можно ознакомиться в документации — некоторые сочетания не соответствуют общим рекомендациям Microsoft (например, F10 открывает окно котировок, а не активирует главное меню).

В параметрах события CHARTEVENT_KEYDOWN содержится следующая информация:

  • lparam — код нажатой клавиши;
  • dparam — количество нажатий клавиши, сгенерированных за время ее удержания в нажатом состоянии;
  • sparam — битовая маска, описывающая статус клавиш клавиатуры, преобразованная в строку.

Биты

Описание

0–7

Скан-код клавиши (зависит от аппаратной части, OEM)

8

Признак клавиши расширенной клавиатуры

9–12

Для служебных целей Windows (не использовать)

13

Состояние клавиши Alt (1 нажата, 0 отжата), недоступно (см. ниже)

14

Предыдущее состояние клавиши (1 если была нажата, 0 если была отжата)

15

Измененное состояние клавиши (1 если отпускается, 0 если нажимается)

Состояние клавиши Alt на самом деле недоступно, т.к. перехватывается терминалом, и этот бит всегда равен 0. Бит 15 также всегда равен 0 из-за контекста срабатывания данного события — в MQL-программу передаются только нажатия, но не отжатия клавиш.

Признак расширенной клавиатуры (бит 8) устанавливается, например для клавиш числового блока (на ноутбуках обычно активируется по Fn), клавиш типа NumLock, ScrollLock, правого Ctrl (в отличие от левого, основного Ctrl) и так далее. Подробнее об этом читайте в документации Windows.

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

Следующая структура поможет убедиться в правильности описания битов.

struct KeyState
{
   uchar scancode;
   bool extended;
   bool altPressed;
   bool previousState;
   bool transitionState;
   
   KeyState() { }
   KeyState(const ushort keymask)
   {
      this = keymask// используем перегрузку оператора=
   }
   void operator=(const ushort keymask)
   {
      scancode = (uchar)(0xFF & keymask);
      extended = 0x100 & keymask;
      altPressed = 0x2000 & keymask;
      previousState = 0x4000 & keymask;
      transitionState = 0x8000 & keymask;
   }
};

В MQL-программе её можно использовать так.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      PrintFormat("%lld %lld %4llX"lparam, (ulong)dparam, (ushort)sparam);
      KeyState state[1];
      state[0] =(ushort)sparam;
      ArrayPrint(state);
   }
}

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

#define KEY_SCANCODE(SPARAM) ((uchar)(((ushort)SPARAM) & 0xFF))
#define KEY_EXTENDED(SPARAM) ((bool)(((ushort)SPARAM) & 0x100))
#define KEY_PREVIOUS(SPARAM) ((bool)(((ushort)SPARAM) & 0x4000))

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

Важно отметить, что код в lparam — это один из виртуальных кодов клавиш клавиатуры. Их список можно увидеть в файле MQL5/Include/VirtualKeys.mqh, который поставляется вместе с MetaTrader 5. Вот, например, некоторые из них:

#define VK_SPACE          0x20
#define VK_PRIOR          0x21
#define VK_NEXT           0x22
#define VK_END            0x23
#define VK_HOME           0x24
#define VK_LEFT           0x25
#define VK_UP             0x26
#define VK_RIGHT          0x27
#define VK_DOWN           0x28
...
#define VK_INSERT         0x2D
#define VK_DELETE         0x2E
...
// VK_0 - VK_9 ASCII коды символов '0' - '9' (0x30 - 0x39)
// VK_A - VK_Z ASCII коды символов 'A' - 'Z' (0x41 - 0x5A)

Коды называются виртуальными, потому что соответствующие клавиши могут быть по-разному расположены на разных клавиатурах или даже реализовываться через совместные нажатия вспомогательных клавиш (таких как Fn на ноутбуках). Кроме того, виртуальность имеет и другую сторону: одна и та же клавиша может генерировать различные символы или управляющие воздействия. Например, клавиша с английской буквой 'A' при переключении на русскоязычную раскладку станет соответствовать русской букве 'Ф'. Также каждая из буквенных клавиш "умеет" генерировать заглавную или строчную букву в зависимости от режима CapsLock и состояния клавиш Shift.

В связи с этим для получения символа из кода виртуальной клавиши в MQL5 API существует специальная функция TranslateKey.

short TranslateKey(int key)

Функция возвращает Unicode-символ по переданному виртуальному коду клавиши, учитывая текущий язык ввода и состояние управляющих клавиш.

В случае ошибки будет получено значение -1. Ошибка может возникнуть в том случае, если код не соответствует корректному символу, например, при попытке получить символ для клавиши Shift.

Напомним, что помимо полученного кода нажатой клавиши MQL-программа способна дополнительно Проверить состояние клавиатуры в части управляющих клавиш и режимов. Кстати говоря, константы вида TERMINAL_KEYSTATE_XXX, передаваемые в качестве параметра в функцию TerminalInfoInteger, составлены по принципу 1000 + виртуальный код клавиши. Например, TERMINAL_KEYSTATE_UP равно 1038, потому что VK_UP равно 38 (0x26).

При планировании алгоритмов с реакцией на нажатия клавиш имейте в виду, что терминал может перехватывать многие комбинации клавиш, поскольку они зарезервированы для выполнения определенных действий (ссылка на документацию приводилась выше). В частности, нажатие на пробел открывает поле быстрой навигации по оси времени. MQL5 API позволяет отчасти управлять такой встроенной обработкой клавиатуры и при необходимости отключать — см. раздел Управление мышью и клавиатурой.

Простой безбуферный индикатор EventTranslateKey.mq5 служит демонстрацией данной функции. В его обработчике OnChartEvent для событий CHARTEVENT_KEYDOWN вызывается TranslateKey, чтобы получить допустимый символ Unicode. Если это удается, символ добавляется в строку сообщения, которое выводится в комментарии графика. По нажатию Enter в текст вставляется перевод строки, а по нажатию Backspace — стирается последний символ с конца.

#include <VirtualKeys.mqh>
   
string message = "";
   
void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      if(lparam == VK_RETURN)
      {
         message += "\n";
      }
      else if(lparam == VK_BACK)
      {
         StringSetLength(messageStringLen(message) - 1);
      }
      else
      {
         ResetLastError();
         const ushort c = TranslateKey((int)lparam);
         if(_LastError == 0)
         {
            message += ShortToString(c);
         }
      }
      Comment(message);
   }
}

Вы можете попробовать вводить символы в разных регистрах и разных языках.

Будьте внимательны. Функция возвращает знаковую величину short, в основном, для возможности вернуть код ошибки -1. Однако типом "широкого" двухбайтового символа принято считать беззнаковое целое ushort. Если приемная переменная будет описана как ushort, проверка с использованием -1 (например, c!=-1) будет выдавать предупреждение компилятора "sign mismatch" (требуется явное приведение типа), а другой вариант (c >= 0) вообще ошибочен, так как всегда равен true.

Для того чтобы в сообщение можно было вставлять пробелы между словами, в обработчике OnInit предварительно отключается быстрая навигация, активируемая пробелом.

void OnInit()
{
   ChartSetInteger(0CHART_QUICK_NAVIGATIONfalse);
}

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

Подокна всегда масштабируются автоматически, чтобы уместить всё содержимое, а чтобы поменять масштаб приходится открывать диалог и вводить значения вручную. Иногда необходимость в этом возникает, если в индикаторах в подокне наблюдаются "выбросы" — слишком большие единичные показания, которые мешают анализировать остальные данные нормального (среднего) размера. Кроме того, иногда желательно просто укрупнить картинку, чтобы разобраться с более мелкими деталями.

Чтобы решить эту проблему и позволить пользователю подстраивать масштаб подокна с помощью нажатий клавиш, реализован индикатор SubScaler.mq5. Сам он не имеет буферов и ничего не отображает.

SubScaler должен быть первым индикатором в подокне или, если выразиться более строго, он должен добавляться в подокно до того, как туда будет добавлен интересующий вас рабочий индикатор, масштаб которого нужно контролировать. Чтобы сделать SubScaler первым, его следует набросить на график (в главное окно) и тем самым создать новое подокно, куда уже затем добавить подчиненный индикатор.

В диалоге настроек рабочего индикатора важно включить опцию Наследовать шкалу (на закладке Шкала).

Когда оба индикатора запущены в подокне, можно использовать клавиши стрелок Вверх/Вниз для увеличения/уменьшения масштаба. Если при этом нажата клавиша Shift, то текущий видимый диапазон значений по вертикальной оси смещается вверх или вниз.

Увеличение масштаба означает укрупнение деталей ("наезд камеры"), так что часть данных может выйти за пределы окна. Уменьшение масштаба означает, что общая картинка становится меньше ("отъезд камеры").

Во входных параметрах задается:

  • Начальный максимум — верхняя граница данных при первичном размещении на графике, по умолчанию +1000;
  • Начальный минимум — нижняя граница данных при первичном размещении на графике, по умолчанию -1000;
  • Коэффициент масштабирования — шаг, с которым будет меняться масштаб по нажатию клавиш, значение в диапазоне [0.01 ... 0.5], по умолчанию 0.1;

Минимум и максимум мы вынуждены спрашивать у пользователя, потому что SubScaler не может заранее знать рабочий диапазон значений произвольного стороннего индикатора, который будет добавлен в подокно следующим.

Когда график восстанавливается после запуска новой сессии терминала или загружается tpl-шаблон, SubScaler подхватывает масштаб предыдущего (сохраненного) состояния.

Теперь рассмотрим реализацию SubScaler.

Вышеописанные настройки задаются в соответствующих входных переменных:

input double FixedMaximum = 1000;  // Initial Maximum
input double FixedMinimum = -1000// Initial Minimum
input double _ScaleFactor = 0.1;   // Scale Factor [0.01 ... 0.5]
input bool Disabled = false;

Кроме того переменная Disabled позволяет временно отключить реакцию на клавиатуру у конкретного экземпляра индикатора, чтобы настраивать несколько разных масштабов в разных подокнах (поочередно).

Поскольку входные переменные доступны в MQL5 только на чтение, мы вынуждены объявить еще одну переменную ScaleFactor, чтобы корректировать введенное значение в разрешенный диапазон [0.01 ... 0.5].

double ScaleFactor;

Номер текущего подокна (w) и количество индикаторов в нем (n) хранятся в глобальных переменных: все они заполняются в обработчике OnInit.

int w = -1n = -1;
   
void OnInit()
{
  ScaleFactor = _ScaleFactor;
  if(ScaleFactor < 0.01 || ScaleFactor > 0.5)
  {
    PrintFormat("ScaleFactor %f is adjusted to default value 0.1,"
       " valid range is [0.01, 0.5]"ScaleFactor);
    ScaleFactor = 0.1;
  }
  w = ChartWindowFind();
  n = ChartIndicatorsTotal(0w);
}

В функции OnChartEvent обрабатываем два типа событий: об изменении графика и с клавиатуры. Событие CHARTEVENT_CHART_CHANGE необходимо для того, чтобы отслеживать добавление в подокно следующего индикатора (рабочего, подлежащего изменению масштаба). Мы при этом запрашиваем текущий диапазон значений подокна (CHART_PRICE_MIN, CHART_PRICE_MAX) и определяем, не является ли он вырожденным, то есть когда и максимум, и минимум равны нулю. В этом случае необходимо применить указанные во входных параметрах начальные пределы (FixedMinimum, FixedMaximum).

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   switch(id)
   {
   case CHARTEVENT_CHART_CHANGE:
      if(ChartIndicatorsTotal(0w) > n)
      {
         n = ChartIndicatorsTotal(0w);
         const double min = ChartGetDouble(0CHART_PRICE_MINw);
         const double max = ChartGetDouble(0CHART_PRICE_MAXw);
         PrintFormat("Change: %f %f %d"minmaxn);
         if(min == 0 && max == 0)
         {
            IndicatorSetDouble(INDICATOR_MINIMUMFixedMinimum);
            IndicatorSetDouble(INDICATOR_MAXIMUMFixedMaximum);
         }
      }
      break;
   ...
   }
}

При получении события нажатия клавиатуры вызывается основная функция Scale, в которую передается не только lparam, но и состояние клавиши Shift, получаемое путем обращения к TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT).

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
  switch(id)
  {
    case CHARTEVENT_KEYDOWN:
      if(!Disabled)
         Scale(lparamTerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT));
      break;
    ...
  }
}

Внутри функции Scale первым делом получаем текущий диапазон значений в переменные min и max.

void Scale(const long cmdconst int shift)
{
   const double min = ChartGetDouble(0CHART_PRICE_MINw);
   const double max = ChartGetDouble(0CHART_PRICE_MAXw);
   ...

Далее в зависимости от того, нажата ли в данный момент клавиша Shift, выполняется либо изменения масштаба, либо панорамирование, то есть сдвиг видимого диапазона значений вверх или вниз. В обоих случаях модификация производится с заданным шагом (множителем) ScaleFactor, относительно пределов min и max, и они назначаются свойствам индикатора INDICATOR_MINIMUM и INDICATOR_MAXIMUM, соответственно. Из-за того, что в "ведомом" индикаторе сделана настройка Наследовать шкалу, она становится рабочей и для него.

   if((shift & 0x10000000) == 0// Shift не нажат - смена масштаба
   {
      if(cmd == VK_UP// укрупняем (наезд)
      {
         IndicatorSetDouble(INDICATOR_MINIMUMmin / (1.0 + ScaleFactor));
         IndicatorSetDouble(INDICATOR_MAXIMUMmax / (1.0 + ScaleFactor));
         ChartRedraw();
      }
      else if(cmd == VK_DOWN// мельчим (отъезд)
      {
         IndicatorSetDouble(INDICATOR_MINIMUMmin * (1.0 + ScaleFactor));
         IndicatorSetDouble(INDICATOR_MAXIMUMmax * (1.0 + ScaleFactor));
         ChartRedraw();
      }
   }
   else // Shift нажат - панорамирование/свдиг диапазона
   {
      if(cmd == VK_UP// сдвигаем диаграммы вверх
      {
         const double d = (max - min) * ScaleFactor;
         IndicatorSetDouble(INDICATOR_MINIMUMmin - d);
         IndicatorSetDouble(INDICATOR_MAXIMUMmax - d);
         ChartRedraw();
      }
      else if(cmd == VK_DOWN// сдвигаем диаграммы вниз
      {
         const double d = (max - min) * ScaleFactor;
         IndicatorSetDouble(INDICATOR_MINIMUMmin + d);
         IndicatorSetDouble(INDICATOR_MAXIMUMmax + d);
         ChartRedraw();
      }
   }
}

При любом изменении вызывается ChartRedraw, чтобы обновить график.

Посмотрим, как SubScaler работает со стандартным индикатором объемов (любые другие индикаторы, включая пользовательские, управляются аналогично).

Различный масштаб, заданный индикаторами SubScaler в двух подокнах
Различный масштаб, заданный индикаторами SubScaler в двух подокнах

Здесь в двух подокнах два экземпляра SubScaler применяют к объемам различный вертикальный масштаб.