利用 MQL5 的交互式 GUI 改进您的交易图表(第 II 部分):可移动 GUI(II)·综合运用
(3/3)· 当图表要挂三个可移动仪表板,复制全局变量会让代码瞬间失控,本篇给出精简解法
很多交易者写 EA 时图省事,直接在 .mq5 里硬抄可移动面板代码,每加一个仪表板就多六个全局变量。等挂到第三个,改一处逻辑要翻遍文件,编译错了都难定位。类的意义就是把这种混乱收进可复用的壳里。
「把仪表板逻辑塞进 .mqh 类里」
在 MT5 里做多仪表板 EA,最忌主文件越写越肿。把创建和拖拽逻辑收进 .mqh 头文件,主 .mq5 只留两个实例声明,后续维护会轻很多。 先动类内部可见性:原来公开暴露的 string Name 在主文件里根本用不到,只服务于头文件内的对象创建,因此把它从 public 改成 private。这一步只是换了个修饰符位置,不影响任何运行时行为。 接着在类里加一个公开方法 CreateDashboard(string name, int xDis, int yDis, int xSize, int ySize),把原先散落在主文件的 ObjectCreate 代码整段搬进全局空间的函数定义。主文件侧只需 #include "GUI_Movable.mqh",然后声明 GUI_Movable Dashboard1、Dashboard2 两个实例。 OnInit() 里分别调 Dashboard1.CreateDashboard(...) 与 Dashboard2.CreateDashboard(...),传入名称与像素坐标、尺寸,返回 INIT_SUCCEEDED 即初始化完成;OnChartEvent() 里把 id、lparam、dparam、sparam 原样甩给两个实例的 OnEvent(),各自按类内逻辑独立响应鼠标。 实测同一图表跑两个可移动矩形标签,先创建的 Dashboard1 在图层层级上低于后创建的 Dashboard2——点选遮挡时这个顺序会直接决定哪个先被命中。外汇与贵金属图表挂 EA 属高风险操作,多对象事件处理前建议在策略测试器先跑一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class GUI_Movable | class=class="str">"cmt">//+------------------------------------------------------------------+ class GUI_Movable { class="kw">private: class="type">int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; class="type">bool movingState; class="kw">public: GUI_Movable() : previousMouseState(class="num">0), mlbDownX(class="num">0), mlbDownY(class="num">0), mlbDownXDistance(class="num">0), mlbDownYDistance(class="num">0), movingState(class="kw">false), Name(NULL) {}; class="type">class="kw">string Name; class="type">void OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class GUI_Movable | class=class="str">"cmt">//+------------------------------------------------------------------+ class GUI_Movable { class="kw">private: class="type">int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; class="type">bool movingState; class="type">class="kw">string Name; class="kw">public: GUI_Movable() : previousMouseState(class="num">0), mlbDownX(class="num">0), mlbDownY(class="num">0), mlbDownXDistance(class="num">0), mlbDownYDistance(class="num">0), movingState(class="kw">false), Name(NULL) {}; class="type">void OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam); }; class="type">class="kw">string Name; class="kw">public: class="type">void OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam); class="type">void CreateDashboard(class="type">class="kw">string name, class="type">int xDis, class="type">int yDis, class="type">int xSize, class="type">int ySize); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GUI_Movable::CreateDashboard(class="type">class="kw">string name, class="type">int xDis, class="type">int yDis, class="type">int xSize, class="type">int ySize) { class=class="str">"cmt">//Create a Rectangle Label Object at(time1, price1)=(class="num">0,class="num">0)
用矩形标签搭可拖拽面板
想在 MT5 图表上放一块能跟鼠标跑的监控面板,核心是先建 OBJ_RECTANGLE_LABEL 再喂坐标和尺寸。下面这段初始化把两个面板分别钉在距左缘 100px、距上缘 100px 和 350px 的位置,宽高都设 200px,相当于在图表上画出两块 200×200 的画布。 开启 CHART_EVENT_MOUSE_MOVE 是关键一步,不打开就收不到鼠标移动事件,面板永远动不了。类里用 previousMouseState、mlbDownX/Y 等私有变量记录按下左键那一刻的鼠标点和标签距离,以此算拖拽偏移。 把代码丢进 EA 的 OnInit 和 OnChartEvent,编译挂到 EURUSD 或 XAUUSD 图表上,就能看到两个方块跟随鼠标拖动。外汇和贵金属波动剧烈、杠杆风险高,这类面板只做信息呈现,别当成下单信号。
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, xDis); 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, yDis); class=class="str">"cmt">//Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(class="num">0, name, OBJPROP_XSIZE, xSize); class=class="str">"cmt">//Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(class="num">0, name, OBJPROP_YSIZE, ySize); class=class="str">"cmt">//Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, true); class=class="str">"cmt">//Give dashboard&class="macro">#x27;s name to the class instance Name = name; class=class="str">"cmt">//Redraw Chart ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "GUI_Movable.mqh" GUI_Movable Dashboard1; GUI_Movable Dashboard2; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { Dashboard1.CreateDashboard("Dashboard1", class="num">100, class="num">100, class="num">200, class="num">200); Dashboard2.CreateDashboard("Dashboard2", class="num">100, class="num">350, class="num">200, class="num">200); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } 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) { Dashboard1.OnEvent(id, lparam, dparam, sparam); Dashboard2.OnEvent(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class GUI_Movable | class=class="str">"cmt">//+------------------------------------------------------------------+ class GUI_Movable { class="kw">private: class="type">int previousMouseState, mlbDownX, mlbDownY, mlbDownXDistance, mlbDownYDistance; class="type">bool movingState; class="type">class="kw">string Name; class="kw">public: GUI_Movable() : previousMouseState(class="num">0), mlbDownX(class="num">0), mlbDownY(class="num">0), mlbDownXDistance(class="num">0), mlbDownYDistance(class="num">0), movingState(class="kw">false), Name(NULL) {}; class="type">void OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam);
◍ 用鼠标拖拽接管面板坐标
在 MT5 里做可拖拽盯盘面板,核心是把图表鼠标事件接进自定义类的 OnEvent。只处理 CHARTEVENT_MOUSE_MOVE,其他事件直接放过,否则会拖慢图表响应。 左键落点的捕获逻辑很关键:previousMouseState 为 0 且 MouseState 为 1,代表这是鼠标左键首次按下。此时把当前 X、Y 以及面板初始的 XDistance、YDistance 存进 mlbDown 系列变量,作为拖拽基准。 命中判定用边界框:X 在 [XDistance, XDistance+XSize]、Y 在 [YDistance, YDistance+YSize] 内才把 movingState 置真。初始化时 XDistance/YDistance 通常为 100,XSize/YSize 为 200,所以面板默认占住图表左上 100,100 到 300,300 的区域。 拖拽中先 ChartSetInteger(0, CHART_MOUSE_SCROLL, false) 锁掉图表自身滚动,再用 ObjectSetInteger 把 XDistance 改成 mlbDownXDistance + X - mlbDownX。外汇与贵金属波动剧烈,这类自绘面板仅作辅助,实盘前请在模拟盘验证交互是否跟手。
class="type">void GUI_Movable::OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, 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) { 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 = Name; class="type">int XDistance = (class="type">int)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 = (class="type">int)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 = (class="type">int)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 = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_YSIZE); class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit() if(previousMouseState == class="num">0 && MouseState == class="num">1) class=class="str">"cmt">//Check if this was the MLB first click { mlbDownX = X; class=class="str">"cmt">//Set mlbDownX(Variable that stores the initial MLB X location) equal to the current X mlbDownY = Y; class=class="str">"cmt">//Set mlbDownY(Variable that stores the initial MLB Y location) equal to the current Y mlbDownXDistance = XDistance; class=class="str">"cmt">//Set mlbDownXDistance(Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance mlbDownYDistance = YDistance; class=class="str">"cmt">//Set mlbDownYDistance(Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance if(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) class=class="str">"cmt">//Check if the click was on the dashboard { movingState = true; class=class="str">"cmt">//If yes the set movingState to True } } if(movingState)class=class="str">"cmt">//if movingState is true, Update the Dashboard position { ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, class="kw">false);class=class="str">"cmt">//Restrict Chart to be moved by Mouse ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);class=class="str">"cmt">//Update XDistance to: mlbDownXDistance + (X - mlbDownX)
「拖完面板记得把图表滚动还回去」
鼠标左键松开后,代码里把 movingState 重置为 false,同时用 ChartSetInteger(0, CHART_MOUSE_SCROLL, true) 把图表滚动权限交还 MT5。若不恢复这项,用户拖完自定义面板会发现滚轮缩放和拖拽平移全部失效,体验直接崩掉。 前面的拖拽逻辑里,ObjectSetInteger 动态调整 OBJPROP_YDISTANCE,新值为 mlbDownYDistance + (Y - mlbDownY),即按下时的纵向偏移加上鼠标实时位移差,再调 ChartRedraw(0) 重绘。这套计算只改标签对象坐标,不动价格坐标,外汇或贵金属图表上叠这类 UI 有滑点误触可能,实盘前先在模拟盘验证。 CreateDashboard 负责初始化矩形标签:ObjectCreate 建 OBJ_RECTANGLE_LABEL,XY 距离和尺寸由入参 xDis/yDis/xSize/ySize 控制,例如传 100/100/200/200 就是距左上角各 100 像素、宽高各 200 像素的面板。CHART_EVENT_MOUSE_MOVE 必须置 true,否则收不到鼠标移动事件,拖动根本不会触发。
ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);class=class="str">"cmt">//Update YDistance to: mlbDownYDistance + (Y - mlbDownY) ChartRedraw(class="num">0); class=class="str">"cmt">//Redraw Chart } if(MouseState == class="num">0)class=class="str">"cmt">//Check if MLB is not pressed { movingState = class="kw">false;class=class="str">"cmt">//set movingState again to class="kw">false ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true);class=class="str">"cmt">//allow the cahrt to be moved again } previousMouseState = MouseState;class=class="str">"cmt">//update the previousMouseState at the end so that we can use it next time and copare it with new value } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GUI_Movable::CreateDashboard(class="type">class="kw">string name, class="type">int xDis, class="type">int yDis, class="type">int xSize, class="type">int ySize) { 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, xDis); 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, yDis); class=class="str">"cmt">//Set XSize to 200px i.e. Width of Rectangle Label ObjectSetInteger(class="num">0, name, OBJPROP_XSIZE, xSize); class=class="str">"cmt">//Set YSize to 200px i.e. Height of Rectangle Label ObjectSetInteger(class="num">0, name, OBJPROP_YSIZE, ySize); class=class="str">"cmt">//Set CHART_EVENT_MOUSE_MOVE to true to detect mouse move event ChartSetInteger(class="num">0, CHART_EVENT_MOUSE_MOVE, true); class=class="str">"cmt">//Give dashboard&class="macro">#x27;s name to the class instance Name = name; class=class="str">"cmt">//Redraw Chart ChartRedraw(); }
别急着下结论
把已有 EA 或指标里的仪表板改成可拖拽,其实只差几行代码:包含 GUI_Movable.mqh、建一个类实例、把面板名字绑上去,鼠标就能拉动。原文示例里 clsSample 从私有继承改成 public clsGUI 后,CreatePanel 的私有访问报错立刻消失,这是评论区实测过的修法。 继承方式直接决定子类能不能碰父类方法:public 继承保留 is-a 关系,protected 和 private 会把父类成员降级,写面板子类前先定好限定符,否则编译期就卡住。 外汇与贵金属波动剧烈、杠杆风险高,这类 GUI 改造只解决交互效率,不替代任何交易逻辑。开 MT5 把那两行 class 定义切换一下,体会访问权限的差异,比看十遍说明来得实在。
class clsSample : clsGUI class clsSample : class="kw">public clsGUI