图形界面 X: 多行文本框控件 (集成编译 8)·进阶篇
(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 与这些宏比对就能区分单击和长按。外汇与贵金属杠杆高、波动剧烈,任何基于热键的手动下单辅助都只是工具,不预示方向,实盘前请在策略测试器里跑通逻辑。
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。外汇与贵金属品种波动剧烈、杠杆风险高,这类键盘触发逻辑仅作辅助,不代表任何方向判断。
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 目录试一次,就能验证按键码到字符的映射是否符合你的输入框预期。
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 交互逻辑都应先在模拟盘验证,避免实盘误触导致非预期下单。
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 等配套文件而报错——若路径理顺后零报错,说明依赖拼装正确,可继续往里填文本缓冲逻辑。外汇与贵金属图表上挂载此类控件存在平台限制与高风险,实盘前务必在策略测试器充分验证。
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">//--- 改变窗口右边缘的宽度