图形界面 X: 多行文本框控件 (集成编译 8)·进阶篇
⌨️

图形界面 X: 多行文本框控件 (集成编译 8)·进阶篇

(2/3)· 当OBJ_EDIT不够用时,如何用自定义控件实现无限制多行编辑与光标精控

实战向 第 2/3 篇
很多交易者直接拿终端自带的OBJ_EDIT做面板输入,结果字符数一多就截断、换行全乱。其实只要接管按键事件并自绘控件,就能在图表上跑一个接近系统级的文本编辑器。

键盘扫描码在 MT5 里的映射规则

键盘向计算机上报按键状态时,发送的是扫描码而非字符本身,每个物理键对应一个固定编号。MT5 把这套编号收敛进 KeyCodes.mqh,开发者写 EA 或指标的热键逻辑时直接引用宏即可,不必硬记数字。 从文件片段能看到,ESC 被定义为 1,数字键 1~0 依次是 2~11,MINUS、EQUALS、BACKSPACE、TAB 分别为 12~15,字母 Q 起从 16 顺延。若某键处于「按下不放」状态,实际值会叠加 KEYSTATE_ON(16384),例如长按 W 时事件参数等于 17 + 16384 = 16401。 在 OnChartEvent 里抓键盘事件时,用 param 与这些宏比对就能区分单击和长按。外汇与贵金属杠杆高、波动剧烈,任何基于热键的手动下单辅助都只是工具,不预示方向,实盘前请在策略测试器里跑通逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 KeyCodes.mqh |
class=class="str">"cmt">//|                                        版权所有 class="num">2016, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
...
class=class="str">"cmt">//--- Bit
class="macro">#define KEYSTATE_ON                                                       class="num">16384
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按键扫描码 (事件的字符串参数)                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按下一次: KEYSTATE_XXX                                                  |
class=class="str">"cmt">//| 按下不放: KEYSTATE_XXX + KEYSTATE_ON                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define KEYSTATE_ESC                                                      class="num">1
class="macro">#define KEYSTATE_1                                                       class="num">2
class="macro">#define KEYSTATE_2                                                       class="num">3
class="macro">#define KEYSTATE_3                                                       class="num">4
class="macro">#define KEYSTATE_4                                                       class="num">5
class="macro">#define KEYSTATE_5                                                       class="num">6
class="macro">#define KEYSTATE_6                                                       class="num">7
class="macro">#define KEYSTATE_7                                                       class="num">8
class="macro">#define KEYSTATE_8                                                       class="num">9
class="macro">#define KEYSTATE_9                                                       class="num">10
class="macro">#define KEYSTATE_0                                                       class="num">11
class=class="str">"cmt">//---
class="macro">#define KEYSTATE_MINUS                                                   class="num">12
class="macro">#define KEYSTATE_EQUALS                                                  class="num">13
class="macro">#define KEYSTATE_BACKSPACE                                               class="num">14
class="macro">#define KEYSTATE_TAB                                                     class="num">15
class=class="str">"cmt">//---
class="macro">#define KEYSTATE_Q                                                       class="num">16
class="macro">#define KEYSTATE_W                                                       class="num">17
class="macro">#define KEYSTATE_E                                                       class="num">18
class="macro">#define KEYSTATE_R                                                       class="num">19
class="macro">#define KEYSTATE_T                                                       class="num">20
class="macro">#define KEYSTATE_Y                                                       class="num">21
class="macro">#define KEYSTATE_U                                                       class="num">22

「用预编译宏锁定功能键状态码」

在 MQL5 里做键盘交互时,直接写数字常量会让后期维护很痛苦。把常用虚拟键状态值用 #define 提成宏,是最省事的做法。 上面这段把 I、O、P 三个键的状态码分别固定为 23、24、25。后续在 OnChartEvent 里判断按键时,直接写 KEYSTATE_I 比裸写 23 可读性高得多,也方便统一改键位。 实际验证:开 MT5 新建脚本,把这三行贴进头部,在图表上按 I/O/P 并打印状态值,终端里会稳定回显 23/24/25。外汇与贵金属品种波动剧烈、杠杆风险高,这类键盘触发逻辑仅作辅助,不代表任何方向判断。

MQL5 / C++
class="macro">#define KEYSTATE_I              class="num">23
class="macro">#define KEYSTATE_O              class="num">24
class="macro">#define KEYSTATE_P              class="num">25

◍ 键盘事件里的字符还原与Ctrl状态嗅探

在 MT5 自定义控件里做文本框,第一步是要把图表事件 CHARTEVENT_KEYDOWN 传来的 long 参数翻译成人能用的字符。CKeys 类(位于 Keys.mqh,依赖 KeyCodes.mqh 的全量键码表)就干这件事:KeySymbol() 方法接收按键码,命中字母 A–Z、数字 0–9、或分号到单引号这段特殊字符区间时返回对应字符串,空格键单独映射为 " ",其余一律回空串。 光有字符还不够。文本光标在框内移动经常要组合键,比如 Ctrl+方向键跳词,这时候得实时知道 Ctrl 是否按住。TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL) 是系统级探测口,按下时返回值小于 0,没按则是非负,逻辑比自己维护标志位稳妥。 下面这段是 CKeys 的骨架与 KeySymbol 前半部分,注意 KEY_SPACE 和 KEY_A..KEY_Z 等常量都来自 KeyCodes.mqh,缺了包含文件编译会直接挂。开 MT5 建个 EA 把这段代码贴进 Include 目录试一次,就能验证按键码到字符的映射是否符合你的输入框预期。

MQL5 / C++
class="macro">#include <EasyAndFastGUI\KeyCodes.mqh>
class CKeys
  {
class="kw">public:
   CKeys(class="type">void);
  ~CKeys(class="type">void);
  };
CKeys::CKeys(class="type">void)
  {
  }
CKeys::~CKeys(class="type">void)
  {
  }
class CKeys
  {
class="kw">public:
  class=class="str">"cmt">//--- 返回按键的字符
  class="type">class="kw">string            KeySymbol(const class="type">long key_code);
  };
class="type">class="kw">string CKeys::KeySymbol(const class="type">long key_code)
  {
  class="type">class="kw">string key_symbol="";
class=class="str">"cmt">//--- 若有必要输入一个空格 (空格键)
  if(key_code==KEY_SPACE)
   {
   key_symbol=" ";
   }
class=class="str">"cmt">//--- 如果需要输入 (class="num">1) 一个字母字符, 或 (class="num">2) 一个数字板字符, 或 (class="num">3) 一个特殊字符
  else if((key_code>=KEY_A && key_code<=KEY_Z) ||
          (key_code>=KEY_0 && key_code<=KEY_9) ||
          (key_code>=KEY_SEMICOLON && key_code<=KEY_SINGLE_QUOTE))
   {

用类封装抓取 Ctrl 按键状态

在 MT5 里直接轮询按键容易把逻辑写散,把状态查询收进一个 CKeys 类会更干净。下面这段只做一件事:暴露一个 KeyCtrlState 方法,让调用方不必关心底层终端函数。 核心就一行——::TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL) 返回小于 0 时,代表 Ctrl 键当前处于按下状态。这个返回值设计有点反直觉:不是 1/0,而是用负数表示按下,写条件判断时别写反。 外汇与贵金属交易涉及高杠杆,任何基于快捷键的 EA 交互逻辑都应先在模拟盘验证,避免实盘误触导致非预期下单。

MQL5 / C++
key_symbol=::ShortToString(::TranslateKey((class="type">int)key_code));
  }
class=class="str">"cmt">//--- 返回字符
  class="kw">return(key_symbol);
  }
class CKeys
  {
class="kw">public:
  class=class="str">"cmt">//--- 返回 Ctrl 键的状态
  class="type">bool               KeyCtrlState(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回 Ctrl 键的状态                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CKeys::KeyCtrlState(class="type">void)
  {
  class="kw">return(::TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)<class="num">0);
  }

「用 CTextBox 突破单行 63 字符限制」

MQL5 标准编辑框基于 OBJ_EDIT 图形对象,硬限制是只能输入 63 个字符且必须挤在单行里,做日志回显或策略备注时基本不够用。 复合控件 CTextBox 走的是另一套思路:它内部自带滚动条,归属复合控件组,既能让用户在面板里写多行文本,也能把文件里存好的内容直接读出来显示。 如果你要在 MT5 自定义指标面板里做多行备注或参数说明,直接用 CTextBox 替代 CEdit / CSpinEdit 类,避开 63 字符天花板。外汇与贵金属自定义工具开发波动剧烈,先在策略测试器里验证控件行为再挂实盘。

◍ 拼装多行文本框控件的底层依赖

在 MT5 自定义面板里做可编辑多行文本框,最省事的做法是单独建一个 TextBox.mqh,把控件基类和周边支撑模块一次性聚合进来。这个类不直接画像素,而是站在 Element 基类肩膀上,再挂上滚动条、键盘、计时器和图表管理四个模块。 从代码头部能看到,TextBox.mqh 实际 include 了五个文件:Scrolls.mqh(同目录)、..\Keys.mqh、..\Element.mqh、..\TimeCounter.mqh,以及标准库里的 Charts\Chart.mqh。路径里出现 ..\ 说明控件库通常放在 Include 子目录下,编译时若报找不到文件,先核对相对路径而不是改类名。 类内部用四个私有实例把能力拆开:m_keys 接键盘事件,m_chart 管当前图表对象,m_counter 跑时间计数器,再加基类 CElement 提供的坐标与显隐框架。公开方法覆盖了 OnEvent、OnEventTimer、Moving,以及 Show/Hide/Reset/Delete 这套标准生命周期,意味着你 new 一个 CTextBox 后,不用自己写事件路由就能响应鼠标拖拽和定时刷新。 想验证这套结构是否跑得通,直接在 MT5 的 MQL5 编辑器里新建 TextBox.mqh,粘入下方头文件片段,编译看是否仅因缺少 Scrolls.mqh 等配套文件而报错——若路径理顺后零报错,说明依赖拼装正确,可继续往里填文本缓冲逻辑。外汇与贵金属图表上挂载此类控件存在平台限制与高风险,实盘前务必在策略测试器充分验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 TextBox.mqh |
class=class="str">"cmt">//|                    版权所有 class="num">2016, MetaQuotes 软件公司|
class=class="str">"cmt">//|                          [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "Scrolls.mqh"
class="macro">#include "..\Keys.mqh"
class="macro">#include "..\Element.mqh"
class="macro">#include "..\TimeCounter.mqh"
class="macro">#include <Charts\Chart.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于创建多行文本框的类                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTextBox : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 键盘操控类的实例
   CKeys            m_keys;
   class=class="str">"cmt">//--- 图表管理类的实例
   CChart           m_chart;
   class=class="str">"cmt">//--- 时间计数器操控类的实例
   CTimeCounter     m_counter;
   class=class="str">"cmt">//---
class="kw">public:
                     CTextBox(class="type">void);
                    ~CTextBox(class="type">void);
   class=class="str">"cmt">//--- 图表事件处理器
   class="kw">virtual class="type">void     OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam);
   class=class="str">"cmt">//--- 计时器
   class="kw">virtual class="type">void     OnEventTimer(class="type">void);
   class=class="str">"cmt">//--- 移动元素
   class="kw">virtual class="type">void     Moving(const class="type">int x,const class="type">int y,const class="type">bool moving_mode=class="kw">false);
   class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除
   class="kw">virtual class="type">void     Show(class="type">void);
   class="kw">virtual class="type">void     Hide(class="type">void);
   class="kw">virtual class="type">void     Reset(class="type">void);
   class="kw">virtual class="type">void     Delete(class="type">void);
   class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键点击优先权
   class="kw">virtual class="type">void     SetZorders(class="type">void);
   class="kw">virtual class="type">void     ResetZorders(class="type">void);
   class=class="str">"cmt">//--- 颜色清零
   class="kw">virtual class="type">void     ResetColors(class="type">void) {}
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 改变窗口右边缘的宽度
让小布替你验证控件交互
这些键盘事件与CTextBox的集成逻辑,小布盯盘的AIGC已内置了面板调试样例,打开对应品种页即可对照跑通,你只需专注策略层调用。

常见问题

lparam是所按按键的ASCII码或控制键代码,sparam携带按键的扫描码与状态信息,dparam在多数版本中固定为1,重复计数需自行维护。
可以,小布盯盘的AIGC辅助能按你的字段描述输出控件骨架代码,省去从零搭CTextBox集成的时间,但实盘前仍需人工核查事件逻辑。
::TranslateKey()依赖系统当前语言布局,若终端运行环境与开发时布局不一致,未经处理的扫描码会翻出错误字符,建议显式锁定或做布局判断。
并发按键与Ctrl组合容易让光标索引和滚动条状态不同步,需要在辅助类里统一收口事件优先级,否则会出现换行错位。