键盘事件

MQL 程序可通过在 OnChartEvent函数中处理 CHARTEVENT_KEYDOWN 事件来接收来自终端的按键消息。

需要注意的是,仅当图表处于活动状态且获得输入焦点时,才会生成此类事件。

在 Windows 中,焦点指的是当前与用户交互的特定窗口在逻辑和视觉上的选中状态。通常,可通过鼠标点击或特殊键盘快捷键(例如 TabCtrl+Tab)切换焦点,这会使被选中的窗口高亮显示。例如,输入字段将会出现文本光标,列表中的当前行会以另一种颜色标记等等。

在终端中也能观察到类似的视觉效果,尤其是当 Market Watch窗口、Data Window 窗口或专家日志获得焦点时。但图表窗口的情况略有不同。从外观上往往难以判断显示前台显示的图表是否已获得输入焦点。如前所述,可以通过以下方式确保切换焦点:点击所需图表(图表区域,而非窗口标题或边框)或使用快捷键:

  • 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键激活),如 NumLockScrollLock、右侧 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// use operator overload=
   }
   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中的代码为虚拟键盘键码。其列表可在 MetaTrader 5 安装目录下的MQL5/Include/VirtualKeys.mqh文件中查看。例如,部分代码如下:

#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 codes of characters '0' - '9' (0x30 - 0x39)
// VK_A - VK_Z ASCII codes of characters 'A' - 'Z' (0x41 - 0x5A)

这些代码被称为虚拟键码,原因在于,相同功能的按键在不同键盘上的位置可能不同,甚至可能通过组合辅助键实现(如笔记本电脑的Fn键)。此外,虚拟性还体现在另一方面,同一按键可能生成不同符号或控制动作。例如,同一按键在不同的语言布局中可能代表不同的字母。此外,每个字母键根据 CapsLock的模式和 Shift 键的状态,可生成大写或小写字母。

为此,MQL5 API 提供了专门的 TranslateKey函数,用于将虚拟键码转换为相应字符。

short TranslateKey(int key)

该函数会根据当前输入语言和控制键的状态,基于传入的虚拟键码返回对应的 Unicode 字符。

如果发生错误,将返回值 -1。如果键码与正确字符不匹配,例如尝试获取 Shift键的字符时,可能会发生错误。

需要注意,MQL 程序除了能获取按键码外,还可通过控制键和模式 检查键盘状态 。顺便提一下,作为参数传递给TerminalInfoInteger函数的 TERMINAL_KEYSTATE_XXX 形式的常量,是基于 1000 + 虚拟键码的原则。例如,TERMINAL_KEYSTATE_UP 为 1038,因为 VK_UP 是 38 (0x26)。

在设计对按键做出反应的算法时,请记住,终端可能会拦截许多组合键,因为它们被保留用于执行特定操作(上文已提供文档链接)。特别要注意,按下空格键会打开一个用于沿时间轴快速导航的字段。MQL5 API 允许你对这种内置键盘处理进行部分控制,并且在必要时可将其禁用。请参阅 鼠标和键盘控制章节。

简单的无缓冲指标EventTranslateKey.mq5可用于演示该函数的功能。在其针对 CHARTEVENT_KEYDOWN 事件的 OnChartEvent处理程序中,将调用 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)将引发“符号不匹配”的编译器警告(需要显式类型转换),而另一种检查方式 (c >= 0) 通常是错误的,因为其结果始终等于 true

为了能够在消息中插入单词间的空格,在 OnInit处理程序中预先禁用了由空格键触发的快速导航功能。

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

作为使用键盘事件的一个完整示例,考虑以下应用程序任务。终端用户知道,无需打开设置对话框,使用鼠标即可交互式地更改主图表窗口的刻度,只需在价格刻度上按下鼠标按钮,然后在不松开的情况下向上/向下移动。遗憾的是,此方法不适用于子窗口。

子窗口始终自动缩放以适应所有内容,要更改比例,必须打开对话框并手动输入数值。当子窗口中的指标显示“异常值”时,即过大的单个读数,会干扰对其他正常(中等)尺寸数据的分析,此时就需要进行这种操作。此外,有时为了处理更精细的细节,简单地放大图像即可。

为解决此问题并允许用户通过键盘操作调整子窗口比例,我们实现了SubScalermq5指标。该指标没有缓冲区且不显示任何内容。

SubScaler 必须作为子窗口中的首个指标,或更严格地说,必须在你需要控制刻度的工作指标添加到子窗口之前,先将其添加到该子窗口中。要使 SubScaler成为首个指标,应将其放置于图表(主窗口)上,从而创建新的子窗口,之后可在该子窗口中添加从属指标。

在工作指标的设置对话框中,务必启用 Inherit scale选项(位于 Scale 选项卡上)。

当两个指标同时在子窗口中运行时,你可以使用Up/Down箭头键进行放大/缩小操作。如果按住 Shift键,垂直轴上的当前可见数值范围将向上或向下移动。

放大意味着聚焦细节(“镜头拉近”),因此部分数据可能超出窗口显示范围。缩小则表示整体画面变小(“镜头拉远”)。

输入参数设置如下:

  • 初始最大值 (Initial maximum) - 初次放置在图表上时数据的上限,默认值为 +1000。
  • 初始最小值 (Initial minimum) - 初次放置在图表上时数据的下限,默认值为 -1000。
  • 缩放系数 (Scaling factor ) - 按键时缩放比例的变化步长,取值范围为 [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),并判断该范围是否为无效范围,即最大值和最小值都等于零的情况。如果出现这种情况,则需应用输入参数中指定的初始限制(FixedMinimumFixedMaximum)。

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,还会通过引用 TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) 获取 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函数内部,首先将当前数值范围存入 minmax 变量中。

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

然后,根据当前是否按下Shift键,执行缩放或平移操作,即向上或向下移动可见数值范围。在这两种情况下,修改操作均以给定步长(乘数)ScaleFactor相对于 minmax 限值执行,并分别将结果赋值给指标特性 INDICATOR_MINIMUM 和 INDICATOR_MAXIMUM。由于从属指标设置了“继承比例 (Inherit scale)”选项,因此该设置也会成为其工作设置。

 if((shift &0x10000000) ==0)// Shift is not pressed - scalechange
   {
      if(cmd == VK_UP// enlarge (zoom in)
      {
         IndicatorSetDouble(INDICATOR_MINIMUMmin / (1.0 + ScaleFactor));
         IndicatorSetDouble(INDICATOR_MAXIMUMmax / (1.0 + ScaleFactor));
         ChartRedraw();
      }
      else if(cmd == VK_DOWN// shrink (zoom out)
      {
         IndicatorSetDouble(INDICATOR_MINIMUMmin * (1.0 + ScaleFactor));
         IndicatorSetDouble(INDICATOR_MAXIMUMmax * (1.0 + ScaleFactor));
         ChartRedraw();
      }
   }
   else // Shift pressed - pan/shift range
   {
      if(cmd == VK_UP// shifting charts up
      {
         const double d = (max - min) * ScaleFactor;
         IndicatorSetDouble(INDICATOR_MINIMUMmin - d);
         IndicatorSetDouble(INDICATOR_MAXIMUMmax - d);
         ChartRedraw();
      }
      else if(cmd == VK_DOWN// shifting charts down
      {
         const double d = (max - min) * ScaleFactor;
         IndicatorSetDouble(INDICATOR_MINIMUMmin + d);
         IndicatorSetDouble(INDICATOR_MAXIMUMmax + d);
         ChartRedraw();
      }
   }
}

每次修改后都会调用 ChartRedraw以更新图表。

让我们看看 SubScaler如何与标准成交量指标配合使用(其他任何指标,包括自定义指标,都以相同方式控制)。

SubScaler 指标在两个子窗口中设置的不同刻度
SubScaler 指标在两个子窗口中设置的不同刻度

在此处两个子窗口中,两个 SubScaler实例分别为成交量指标应用了不同的垂直刻度。