「脚本里也能吃键盘事件」
多数人以为 MQL5 里只有 EA 的 OnChartEvent 能拦键盘,其实脚本虽不直接支持这个处理程序,也能靠机制间接控对象属性。上一篇文章只演示了图表交互时默认触发的按键事件,那种拦截太常规,容易让人误以为键盘就这么一种用法。 MT5 的设计初衷是处理价格图表,不是专门接键盘输入,所以用脚本吃键位属于偏门路径。但知道哪些能做、哪些被限制住,对写自定义工具的人很关键——比如你开 MT5 挂个脚本想改对象颜色,不摸清边界就会卡在事件收不到。 这类用法不常见,却能在没有 EA 常驻时替你省手动调整的功夫。高风险提示:外汇与贵金属杠杆交易本身风险极高,脚本误操作对象不影响仓位,但别把它当交易信号源。
脚本里硬造键盘事件的弯弯绕
MQL5 的脚本(Script)生命周期里压根没有 OnChartEvent 这种事件入口,也就是说原生不支持键盘事件回调。但我们可以绕开事件模型,用 TerminalInfoInteger 去轮询按键状态,在脚本内部伪造一套类似 CHARTEVENT_KEYDOWN 的处理逻辑。 直接用 OnInit+OnChartEvent 的 EA 写法(代码 01)里,第 33~49 行靠方向键上下移动十字线锚定的 K 线。脚本没有这套事件,只能把同样的对象创建和移动逻辑抽出来,改用 while(!IsStopped()) 循环去查键。 代码 02 是最直白的 OnStart:创建垂直线和水平线、移动一次、马上删除。跑起来你啥也看不见,因为创建到删除在同一帧完成,没有交互窗口。代码 03 把 Init / KeyEvent / Deinit 拆成函数,结构清朗了,但依然没循环,不会动。 真正能跑起来的是代码 04:第 14 行 while 循环里,第 16、18 行用 TERMINAL_KEYSTATE_UP / DOWN 查按键,第 20 行 Sleep(100) 把 CPU 占用压下去。实测每 100 毫秒轮询一次,键盘上下键就能像 EA 里那样逐根 K 线推十字线,动画 02、03 就是这效果。 别把轮询当无脑死循环。MT5 一切换周期就会把脚本从图表剥离,脚本自己创建的那两条线若不主动 ObjectDelete,重绘后可能还挂在图上。外汇和贵金属品种波动大、周期切换频繁,这种残留对象容易干扰手动下单视线,脚本收尾务必走 Deinit 清场。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_KEY_UP class="num">38 class="macro">#define def_KEY_DOWN class="num">40 class=class="str">"cmt">//+----------------+ class="macro">#define macro_NameObject "Demo" + (class="type">class="kw">string)ObjectsTotal(class="num">0) class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string gl_Objs[class="num">2]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart(class="type">void) { Init(); while (!IsStopped()) { if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) KeyEvent(def_KEY_UP); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) KeyEvent(def_KEY_DOWN); Sleep(class="num">100); } Deinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Init(class="type">void) { ObjectCreate(class="num">0, gl_Objs[class="num">0] = macro_NameObject, OBJ_VLINE, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, gl_Objs[class="num">0], OBJPROP_COLOR, clrRoyalBlue); ObjectCreate(class="num">0, gl_Objs[class="num">1] = macro_NameObject, OBJ_HLINE, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, gl_Objs[class="num">1], OBJPROP_COLOR, clrPurple); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void KeyEvent(class="type">int lparam) { class="kw">static class="type">int p = class="num">0; class="type">MqlRates rate[class="num">1]; class="kw">switch (lparam) { case def_KEY_DOWN: p = (p < Bars(_Symbol, _Period) ? p + class="num">1 : p); break; case def_KEY_UP: p = (p > class="num">0 ? p - class="num">1 : p); break; class="kw">default: class="kw">return; } Comment(StringFormat("Current bar analyzed: %d", p)); CopyRates(_Symbol, _Period, p, rate.Size(), rate); ObjectMove(class="num">0, gl_Objs[class="num">0], class="num">0, rate[class="num">0].time, rate[class="num">0].close); ObjectMove(class="num">0, gl_Objs[class="num">1], class="num">0, rate[class="num">0].time, rate[class="num">0].close); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Deinit(class="type">void) { Comment(""); for (class="type">uint c = class="num">0; c < gl_Objs.Size(); c++) ObjectDelete(class="num">0, gl_Objs[c]); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用函数指针把重复逻辑塞进数组
MQL5 没有直接暴露内存指针,但允许定义指向函数或过程的类型模板,这就是函数指针。它把「函数存在于内存某地址」这件事封装成一种数据类型,让你能在不直接写死函数名的前提下,动态决定调用谁。 基础形态很简单:先用 typedef 声明签名,再把函数名当值赋给同类型数组元素。下面这段是最小可跑样例,两个函数返回字符串,仅名字不同。
typedef class="type">class="kw">string (*FnPtr)(const class="type">int); class="type">class="kw">string Msg_01(const class="type">int value) { class="kw">return "Example message made in function " + __FUNCTION__ + " with the value " + (class="type">class="kw">string)value; } class="type">class="kw">string Msg_02(const class="type">int value) { class="kw">return "Example message made in function " + __FUNCTION__ + " with the value " + (class="type">class="kw">string)value; } class="type">void OnStart(class="type">void) { FnPtr FnMsg[class="num">2]; FnMsg[class="num">0] = Msg_01; FnMsg[class="num">1] = Msg_02; for (class="type">uchar c = class="num">0, i = class="num">15; c < FnMsg.Size(); c++, i += class="num">30) Print(FnMsg[c](i)); }
typedef string (*FnPtr)(const int); 定义指针类型,要求返回 string、收一个 const int;第 6 行 FnPtr FnMsg[2]; 声明长度为 2 的指针数组;第 7、8 行把 Msg_01 / Msg_02 直接赋给元素,没带括号就是传地址不是调用;第 10 行 FnMsg[c](i) 通过循环索引间接调用,i 从 15 起每次加 30,所以终端会打出 15、45 两组值。
到了实战层,这套机制能把按序执行的一堆步骤收进循环。示例代码里 ProcPtr proc[3] 装了 Init、Loop、Deinit,一个 for 顺序跑完;Loop 内部又用 KeyEvent key[3] 按上下键状态切函数,第 45 行靠三元运算符选下标。外汇 / 贵金属 EA 若用这种写法扩展按键响应或步骤流,改动成本会低很多,但逻辑抽象度高,调试时容易看不出实际调了哪个函数,属于高风险封装方式,上线前务必在策略测试器单步验证。
别把正态当圣经
函数指针不是炫技专用。当你发现 OnStart 里要手抄十几行雷同调用、且顺序常改时,才值得引入;否则平铺直叙反而更好查错。
typedef class="type">class="kw">string (*FnPtr)(const class="type">int); class="type">class="kw">string Msg_01(const class="type">int value) { class="kw">return "Example message made in function " + __FUNCTION__ + " with the value " + (class="type">class="kw">string)value; } class="type">class="kw">string Msg_02(const class="type">int value) { class="kw">return "Example message made in function " + __FUNCTION__ + " with the value " + (class="type">class="kw">string)value; } class="type">void OnStart(class="type">void) { FnPtr FnMsg[class="num">2]; FnMsg[class="num">0] = Msg_01; FnMsg[class="num">1] = Msg_02; for (class="type">uchar c = class="num">0, i = class="num">15; c < FnMsg.Size(); c++, i += class="num">30) Print(FnMsg[c](i)); }
「一点提醒」
鼠标事件会是下一篇的重头戏,键盘交互那部分已经挖得够深,接下来重心自然转到光标在图表上的动作捕获。 把这一辑里的代码示例都跑一遍,再回头翻翻之前讲继承、结构的几篇,想想怎么把那些对象和眼下这套事件框架拼起来用。能动手实现一个把已知知识点串起来的小工具,比单纯读文更有用。 外汇和贵金属行情受杠杆与跳空影响大,任何基于 MT5 对象事件的脚本都只是辅助观察,实盘前务必在模拟环境验证,亏损概率始终存在。