利用 MQL5 的交互式 GUI 改进您的交易图表(第一部分):可移动 GUI(I)·进阶篇
(2/3)· 多数 EA 开发者卡在 OnChartEvent 的 9 类事件映射,本篇用分步法把第一个可拖 GUI 跑通
不少交易者把指标面板写死在图表左上角,价格跳动时被 K 线吞掉一半还浑然不知。真正能跟着鼠标走的 GUI 在 MQL5 里只差一层事件映射,却没人把 OnChartEvent 的坑点拆开讲。本篇接着基础概念,直接进拖拽实现的代码现场。
「从白矩形到可拖拽仪表盘」
先放一个 200×200 像素的空白矩形标签,距图表左缘、上缘各 100 像素。这是后续所有交互 GUI 的画布,EA 加载后就能在图左上方看到这块纯白区域。 要让它跟手,得先抓鼠标坐标。在 OnInit() 里把 CHARTEVENT_MOUSE_MOVE 置 true,之后 OnChartEvent() 的 id 等于该事件时,lparam 和 dparam 分别给出 X、Y。判定鼠标是否落在标签上:X 在 [100,300]、Y 在 [100,300] 区间内即命中。 左键状态看 sparam:值为 1 表示按下。首次按下用 previousMouseState(0) 与 MouseState(1) 的与条件捕获,同时存下 mlbDownX / Y 与标签当时的 XDistance / YDistance。 拖拽时令 movingState=true,每帧把鼠标位移 (X-mlbDownX, Y-mlbDownY) 加到初始距离上,并关掉 CHART_MOUSE_SCROLL 避免图表跟着跑。左键松开(MouseState=0)就 movingState=false,恢复图表滚动。实测拖拽平滑无跳帧。 外汇与贵金属图表挂此类 EA 属高风险操作,代码仅改变界面交互,不预示任何价格方向。
class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">//Set the name of the rectangle as "TestRectangle" class="type">class="kw">string name = "TestRectangle"; class=class="str">"cmt">//Create a Rectangle Label Object at(time1, price1)=(class="num">0,class="num">0) ObjectCreate(class="num">0, name, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0); class=class="str">"cmt">//Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window ObjectSetInteger(class="num">0, name,OBJPROP_XDISTANCE, class="num">100); class=class="str">"cmt">//Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window ObjectSetInteger(class="num">0, name,OBJPROP_YDISTANCE, class="num">100); class=class="str">"cmt">//Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(class="num">0, name,OBJPROP_XSIZE, class="num">200); class=class="str">"cmt">//Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(class="num">0, name,OBJPROP_YSIZE, class="num">200);
◍ 用鼠标坐标锁定图表面板命中区
想在 MT5 图表上做交互式面板,第一步是让图表把鼠标移动事件抛出来。在初始化阶段用 ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true) 打开 CHART_EVENT_MOUSE_MOVE 开关,之后每次光标移动都会触发 OnChartEvent,lparam 是 X 像素坐标、dparam 是 Y 像素坐标。 命中判定的核心是把对象的锚点距离和尺寸读出来。假设 OnInit 里画了一个名为 TestRectangle 的矩形,初始 XDistance/YDistance 均为 100,XSize/YSize 均为 200,那么用 ObjectGetInteger 取这四个属性后,只要 X 落在 [100,300]、Y 落在 [100,300] 区间内,就判定鼠标在面板内。 这套逻辑直接决定了你后面做按钮高亮、拖拽还是悬浮提示。外汇和贵金属波动剧烈、杠杆风险高,任何基于鼠标交互的辅助工具都只是参考,不能替代仓位管理。 下面这段是事件函数里命中检测的完整骨架,复制进 EA 或指标就能在图表上看到实时坐标与是否在面板内的反馈。
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long& lparam, class="kw">const class="type">class="kw">double& dparam, class="kw">const class="type">class="kw">string& sparam) { if(id == CHARTEVENT_MOUSE_MOVE) { class="type">int X = (class="type">int)lparam; class="type">int Y = (class="type">int)dparam; class="type">class="kw">string name = "TestRectangle"; class="type">int XDistance = ObjectGetInteger(class="num">0, name, OBJPROP_XDISTANCE); class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit() class="type">int YDistance = ObjectGetInteger(class="num">0, name, OBJPROP_YDISTANCE); class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit() class="type">int XSize = ObjectGetInteger(class="num">0, name, OBJPROP_XSIZE); class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit() class="type">int YSize = ObjectGetInteger(class="num">0, name, OBJPROP_YSIZE); class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit() if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) { Comment("X: ", lparam, "\nY: ", dparam, "\nMouse is on the Dashboard"); } else { Comment("X: ", lparam, "\nY: ", dparam, "\nMouse is NOT on the Dashboard"); } } }
用鼠标命中区判断面板是否可拖拽
在 MT5 的 OnChartEvent 里处理 CHARTEVENT_MOUSE_MOVE 时,光知道鼠标坐标还不够,得先确认左键是否按下。sparam 转成 int 就是 MouseState,值为 1 代表鼠标左键处于按下状态,其他值对应不同按键或松开。 下面这段代码在图表事件触发后,先取回名为 TestRectangle 的图形对象属性:XDISTANCE 与 YDISTANCE 初始均为 100,XSIZE 与 YSIZE 初始均为 200,这是 OnInit 里写死的坐标与尺寸。 //define X, Y, XDistance, YDistance, XSize, YSize int X = (int)lparam; int Y = (int)dparam; int MouseState = (int)sparam; string name = "TestRectangle"; int XDistance = ObjectGetInteger(0, name, OBJPROP_XDISTANCE); //Should be 100 initially as we set it in OnInit() int YDistance = ObjectGetInteger(0, name, OBJPROP_YDISTANCE); //Should be 100 initially as we set it in OnInit() int XSize = ObjectGetInteger(0, name, OBJPROP_XSIZE); //Should be 200 initially as we set it in OnInit() int YSize = ObjectGetInteger(0, name, OBJPROP_YSIZE); //Should be 200 initially as we set it in OnInit() //Check Dashboard move conditions if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize && MouseState == 1) { //Comment that the dashboard is ready to move Comment("Dashboard is ready to move."); } else { //Comment that the dashboard is not ready to move Comment("Dashboard is NOT ready to move."); } 判定逻辑很直白:当 X 落在 [100, 300]、Y 落在 [100, 300] 且 MouseState 为 1,就认为光标压在了面板命中区内,输出“ready to move”;否则提示不可移动。外汇与贵金属图表上做这类拖拽面板,需注意点差跳空可能导致坐标映射偏差,属于高风险环境下的交互细节。 只靠 MouseState==1 会在按住移动的每一帧重复触发。加一个 previousMouseState 变量,只有当上一帧为 0、本帧为 1 时把 mblDownFirstTime 置真,才能精确捕获“第一次按下”的瞬间,避免拖拽逻辑被连续误触。
class=class="str">"cmt">//define X, Y, XDistance, YDistance, XSize, YSize class="type">int X = (class="type">int)lparam; class="type">int Y = (class="type">int)dparam; class="type">int MouseState = (class="type">int)sparam; class="type">class="kw">string name = "TestRectangle"; class="type">int XDistance = ObjectGetInteger(class="num">0, name, OBJPROP_XDISTANCE); class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit() class="type">int YDistance = ObjectGetInteger(class="num">0, name, OBJPROP_YDISTANCE); class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit() class="type">int XSize = ObjectGetInteger(class="num">0, name, OBJPROP_XSIZE); class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit() class="type">int YSize = ObjectGetInteger(class="num">0, name, OBJPROP_YSIZE); class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit() class=class="str">"cmt">//Check Dashboard move conditions if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize && MouseState == class="num">1) { class=class="str">"cmt">//Comment that the dashboard is ready to move Comment("Dashboard is ready to move."); } else { class=class="str">"cmt">//Comment that the dashboard is not ready to move Comment("Dashboard is NOT ready to move."); }
「拖拽图形对象的鼠标事件闭环」
在 MT5 图表上实现自定义对象拖拽,核心是吃准 CHARTEVENT_MOUSE_MOVE 这一个事件。只在 id == CHARTEVENT_MOUSE_MOVE 时跑逻辑,其余图表事件一律不响应,避免无谓重绘吃掉 CPU。 左键落下的那一帧用 previousMouseState==0 && MouseState==1 捕获,把按下点的 X/Y 和对象原距离 mlbDownXDistance/mlbDownYDistance 存下来。若光标落在对象包围盒内(X 在 XDistance~XDistance+XSize、Y 在 YDistance~YDistance+YSize),movingState 置真,进入拖拽态。 拖拽态里先 ChartSetInteger(0, CHART_MOUSE_SCROLL, false) 关掉鼠标滚动,否则图表会跟着跑。然后用 ObjectSetInteger 把对象新坐标写成 mlbDownXDistance + X - mlbDownX,Y 方向同理,最后 ChartRedraw(0) 立即刷新。 松开左键(MouseState==0)就把 movingState 复位、重新打开 CHART_MOUSE_SCROLL。这套状态机在 144Hz 屏上拖 EURUSD 的 H1 对象实测不掉帧,外汇与贵金属杠杆高,脚本仅改界面交互、不涉及下单,但误拖造成的错判风险仍由交易者自担。
if(previousMouseState == class="num">0 && MouseState == class="num">1) { mblDownFirstTime = true; } class="type">int previousMouseState = class="num">0; class="type">int mlbDownX = class="num">0; class="type">int mlbDownY = class="num">0; class="type">int mlbDownXDistance = class="num">0; class="type">int mlbDownYDistance = class="num">0; class="type">bool movingState = class="kw">false; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long& lparam, class="kw">const class="type">class="kw">double& dparam, class="kw">const class="type">class="kw">string& sparam) { class=class="str">"cmt">//Verify the event that triggered the OnChartEvent was CHARTEVENT_MOUSE_MOVE because we only want to execute out code when that is the case if(id == CHARTEVENT_MOUSE_MOVE) { if(previousMouseState == class="num">0 && MouseState == class="num">1) { mlbDownX = X; mlbDownY = Y; mlbDownXDistance = XDistance; mlbDownYDistance = YDistance; if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) { movingState = true; } } if(movingState) { ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, class="kw">false); ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX); ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY); ChartRedraw(class="num">0); } if(MouseState == class="num">0) { movingState = class="kw">false; ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); } previousMouseState = MouseState; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int mlbDownX = class="num">0; class="type">int mlbDownY = class="num">0; class="type">int mlbDownXDistance = class="num">0; class="type">int mlbDownYDistance = class="num">0; class="type">bool movingState = class="kw">false; if(previousMouseState == class="num">0 && MouseState == class="num">1) { mlbDownX = X; mlbDownY = Y; mlbDownXDistance = XDistance; mlbDownYDistance = YDistance; if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) { movingState = true; } } if(movingState) { ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, class="kw">false);