利用 MQL5 的交互式 GUI 改进您的交易图表(第 III 部分):简易可移动交易 GUI·综合运用
(3/3)· 从零拼出可拖拽的简易交易 EA 面板,告别图表上钉死的操作区
按钮类与面板控件的实例化骨架
在 MT5 自定义面板里,按钮不是直接画上去的,而是先封装成类再逐个声明。下面这段把背景色、文字、销毁三个动作收进 Button 类的方法里,调用时只需一行便可改状态。 [CODE] void SetBGColor(color col) {ObjectSetInteger(0, _name, OBJPROP_BGCOLOR, col);} // 设背景色 void SetText(string text) {ObjectSetString(0, _name, OBJPROP_TEXT, text);} // 设文字 void Destroy() {ObjectDelete(0, _name);} // 删按钮 [/CODE] Create 方法用 ObjectCreate 挂 OBJ_BUTTON,并以 XDISTANCE / YDISTANCE / XSIZE / YSIZE 四个整数定坐标和尺寸,默认参数全为 0,意味着不传参时按钮会挤在左上角 (0,0) 且不可见。 [CODE] void Button::Create(string name, int xDis=0, int yDis=0, int xSize=0, int ySize=0) { ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xDis); ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDis); ObjectSetInteger(0, name, OBJPROP_XSIZE, xSize); ObjectSetInteger(0, name, OBJPROP_YSIZE, ySize); _name = name; } [/CODE] 真正拼面板时,代码尾部用 include 把矩形标签、文本、编辑框、按钮的头文件拉进来,再声明 TitleBar、LotSize、BuyButton 等对象——外汇与贵金属图表上叠加这类控件属高风险操作环境,实盘前请在策略测试器里先验证坐标不遮挡 K 线。 开 MT5 新建 ea,把 Button.mqh 的 Create 参数改成 xDis=10, yDis=10, xSize=80, ySize=25,编译后能在左上角看到可点区域,证明类封装通路正常。
class="type">void SetBGColor(class="type">class="kw">color col) {ObjectSetInteger(class="num">0, _name, OBJPROP_BGCOLOR, col);} class=class="str">"cmt">// Method to set the background class="type">class="kw">color class="type">void SetText(class="type">class="kw">string text) {ObjectSetString(class="num">0, _name, OBJPROP_TEXT, text);} class=class="str">"cmt">// Method to set the text content class="type">void Destroy() {ObjectDelete(class="num">0, _name);} class=class="str">"cmt">// Method to destroy a button control }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ Button::Button(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ Button::~Button(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Method to create a button control object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Button::Create(class="type">class="kw">string name, class="type">int xDis = class="num">0, class="type">int yDis = class="num">0, class="type">int xSize = class="num">0, class="type">int ySize = class="num">0) { class=class="str">"cmt">// Code to create button control object, set its position, size, and assign its name ObjectCreate(class="num">0, name, OBJ_BUTTON, class="num">0, class="num">0, class="num">0); ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, xDis); ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, yDis); ObjectSetInteger(class="num">0, name, OBJPROP_XSIZE, xSize); ObjectSetInteger(class="num">0, name, OBJPROP_YSIZE, ySize); _name = name; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "RectangleLabel.mqh" class=class="str">"cmt">// Including the RectangleLabel class definition RectangleLabel TitleBar; class=class="str">"cmt">// Declaration of a TitleBar object RectangleLabel MainDashboardBody; class=class="str">"cmt">// Declaration of a MainDashboardBody object class="macro">#include "Label.mqh" class=class="str">"cmt">// Including the Label class definition Label TitleText; class=class="str">"cmt">// Declaration of a Label object Label LotSizeText; class=class="str">"cmt">// Declaration of a LotSizeText object class="macro">#include "Edit.mqh" class=class="str">"cmt">// Including the Edit class definition Edit LotSize; class=class="str">"cmt">// Declaration of a LotSize object class="macro">#include "Button.mqh" class=class="str">"cmt">// Including the Button class definition Button BuyButton; class=class="str">"cmt">// Declaration of a BuyButton object Button SellButton; class=class="str">"cmt">// Declaration of a SellButton object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function |
◍ 在 OnInit 里搭出面板骨架
EA 启动阶段最实在的一件事,是把交易面板的控件在 OnInit 里一次性建出来。下面这段代码把标题栏、主体框、手数输入和买卖按钮全部按坐标铺好,坐标单位是图表像素,左上角基准为 (100,100)。 TitleBar 与 MainDashboardBody 都用了 BORDER_FLAT,尺寸分别是 200×20 和 200×100,两者纵向错开 19 像素(100 与 119)刚好拼成无重叠的上下结构。TitleText 字号 10、LotSizeText 字号 12,肉眼能看出层级差异。 LotSize 编辑框预设文本 "0.01",背景 clrWhiteSmoke,买入键 clrLime、卖出键 clrRed,颜色直接暗示方向。最后 ChartRedraw(0) 强制重绘,否则 MT5 上可能看不到控件。外汇与贵金属波动剧烈,面板手数默认 0.01 仅为最小风险演示,实盘请按账户净值重算。 别把坐标当绝对真理:不同 DPI 的显示器上 (110,140) 这类硬编码位置可能偏移,开 MT5 用「窗口→重建」验证一下最稳妥。
class="type">int OnInit() { TitleBar.Create("TitleBar", class="num">100, class="num">100, class="num">200, class="num">20); class=class="str">"cmt">// Creating the TitleBar with specified dimensions TitleBar.SetBorderType(BORDER_FLAT); class=class="str">"cmt">// Setting the border type of TitleBar to be flat MainDashboardBody.Create("MainDashboardBody", class="num">100, class="num">119, class="num">200, class="num">100); class=class="str">"cmt">// Creating the MainDashboardBody with specified dimensions MainDashboardBody.SetBorderType(BORDER_FLAT); class=class="str">"cmt">// Setting the border type of MainDashboardBody to be flat TitleText.Create("TitleText", class="num">110, class="num">101); class=class="str">"cmt">// Creating the TitleText at(class="num">110,class="num">101) TitleText.SetText("Simple Trading EA V1.class="num">0"); class=class="str">"cmt">// Setting its text to "Simple Trading EA V1.class="num">0" TitleText.SetFontSize(class="num">10); class=class="str">"cmt">// Setting its font size to class="num">10 TitleText.SetTextColor(clrBlack); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrBlack LotSizeText.Create("LotSizeText", class="num">110, class="num">140); class=class="str">"cmt">// Creating the LotSizeText at(class="num">110,class="num">140) LotSizeText.SetText("Lot Size:"); class=class="str">"cmt">// Setting its text to "Lot Size:" LotSizeText.SetFontSize(class="num">12); class=class="str">"cmt">// Setting its font size to class="num">12 LotSizeText.SetTextColor(clrBlack); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrBlack LotSize.Create("LotSize", class="num">220, class="num">140, class="num">50, class="num">20); class=class="str">"cmt">// Creating the LotSize with specified dimensions LotSize.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack LotSize.SetBGColor(clrWhiteSmoke); class=class="str">"cmt">// Setting its BG Color to clrWhiteSmoke LotSize.SetText("class="num">0.01"); class=class="str">"cmt">// Setting its text to class="num">0.01 LotSize.SetTextColor(clrBlack); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrBlack BuyButton.Create("BuyButton", class="num">110, class="num">180, class="num">80, class="num">25); class=class="str">"cmt">// Creating the BuyButton with specified dimensions BuyButton.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack BuyButton.SetText("Buy"); class=class="str">"cmt">// Setting its text to "Buy" BuyButton.SetBGColor(clrLime); class=class="str">"cmt">// Setting its BG Color to clrLime SellButton.Create("SellButton", class="num">210, class="num">180, class="num">80, class="num">25); class=class="str">"cmt">// Creating the SellButton with specified dimensions SellButton.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack SellButton.SetText("Sell"); class=class="str">"cmt">// Setting its text to "Sell" SellButton.SetBGColor(clrRed); class=class="str">"cmt">// Setting its BG Color to clrRed ChartRedraw(class="num">0); class=class="str">"cmt">// Redrawing the chart to reflect changes class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// Indicating successful initialization }
「手动拆解面板的对象销毁与初始化」
在 MT5 自定义 EA 面板里,所有图形对象用完必须显式 Destroy,否则图表上会残留不可控的矩形和按钮。下面这段析构逻辑在退出时依次清理主面板、标题栏、文字标签和交易按钮,顺序和创建时相反能降低 GUI 资源泄漏概率。 MainDashboardBody.Destroy(); // 销毁主面板背景对象 TitleBar.Destroy(); // 销毁标题栏矩形 TitleText.Destroy(); // 销毁标题文字 LotSizeText.Destroy(); // 销毁手数说明文字 LotSize.Destroy(); // 销毁手数输入框 BuyButton.Destroy(); // 销毁买入按钮 SellButton.Destroy(); // 销毁卖出按钮 面板的实际搭建放在 OnInit 里。TitleBar 用 Create("TitleBar",100,100,200,20) 在坐标(100,100)画了宽200高20的扁平矩形,背景色 C'27,59,146'(深蓝);主面板 MainDashboardBody 从 y=119 向下延伸100像素,色值 C'102,152,250'(浅蓝),两者拼起来就是经典悬浮窗轮廓。 文字层叠加在矩形之上:TitleText 字号10、白字写「Simple Trading EA V1.0」;LotSizeText 字号12提示「Lot Size:」;可编辑框 LotSize 位于(220,140)、宽50高20,黑边白底方便手动填手数。外汇与贵金属波动剧烈、杠杆风险高,这类面板仅作下单辅助,不替代风控。 想验证可直接把 RectangleLabel.mqh / Label.mqh / Edit.mqh / Button.mqh 丢进 Include 目录,编译后挂 EURUSD 图表看坐标是否如代码所述。
MainDashboardBody.Destroy(); class=class="str">"cmt">// Destroying the MainDashboardBody object TitleBar.Destroy(); class=class="str">"cmt">// Destroying the TitleBar object TitleText.Destroy(); class=class="str">"cmt">// Destroying the TitleText object LotSizeText.Destroy(); class=class="str">"cmt">// Destroying the LotSizeText object LotSize.Destroy(); class=class="str">"cmt">// Destroying the LotSize object BuyButton.Destroy(); class=class="str">"cmt">// Destroying the BuyButton object SellButton.Destroy(); class=class="str">"cmt">// Destroying the SellButton object } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "RectangleLabel.mqh" class=class="str">"cmt">// Including the RectangleLabel class definition RectangleLabel TitleBar; class=class="str">"cmt">// Declaration of a TitleBar object RectangleLabel MainDashboardBody; class=class="str">"cmt">// Declaration of a MainDashboardBody object class="macro">#include "Label.mqh" class=class="str">"cmt">// Including the Label class definition Label TitleText; class=class="str">"cmt">// Declaration of a Label object Label LotSizeText; class=class="str">"cmt">// Declaration of a LotSizeText object class="macro">#include "Edit.mqh" class=class="str">"cmt">// Including the Edit class definition Edit LotSize; class=class="str">"cmt">// Declaration of a LotSize object class="macro">#include "Button.mqh" class=class="str">"cmt">// Including the Button class definition Button BuyButton; class=class="str">"cmt">// Declaration of a BuyButton object Button SellButton; class=class="str">"cmt">// Declaration of a SellButton object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { TitleBar.Create("TitleBar", class="num">100, class="num">100, class="num">200, class="num">20); class=class="str">"cmt">// Creating the TitleBar with specified dimensions TitleBar.SetBorderType(BORDER_FLAT); class=class="str">"cmt">// Setting the border type of TitleBar to be flat TitleBar.SetBGColor(C&class="macro">#x27;class="num">27, class="num">59, class="num">146&class="macro">#x27;); class=class="str">"cmt">// Setting the class="type">class="kw">color to RGB code: C&class="macro">#x27;class="num">27, class="num">59, class="num">146&class="macro">#x27; MainDashboardBody.Create("MainDashboardBody", class="num">100, class="num">119, class="num">200, class="num">100); class=class="str">"cmt">// Creating the MainDashboardBody with specified dimensions MainDashboardBody.SetBorderType(BORDER_FLAT); class=class="str">"cmt">// Setting the border type of MainDashboardBody to be flat MainDashboardBody.SetBGColor(C&class="macro">#x27;class="num">102, class="num">152, class="num">250&class="macro">#x27;); class=class="str">"cmt">// Setting the class="type">class="kw">color to RGB code: C&class="macro">#x27;class="num">102, class="num">152, class="num">250&class="macro">#x27; TitleText.Create("TitleText", class="num">110, class="num">101); class=class="str">"cmt">// Creating the TitleBar at(class="num">110,class="num">101) TitleText.SetText("Simple Trading EA V1.class="num">0"); class=class="str">"cmt">// Setting its text to "Simple Trading EA V1.class="num">0" TitleText.SetFontSize(class="num">10); class=class="str">"cmt">// Setting its font size to class="num">10 TitleText.SetTextColor(clrWhite); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrWhite LotSizeText.Create("LotSizeText", class="num">110, class="num">140); class=class="str">"cmt">// Creating the LotSizeText at(class="num">110,class="num">140) LotSizeText.SetText("Lot Size:"); class=class="str">"cmt">// Setting its text to "Lot Size:" LotSizeText.SetFontSize(class="num">12); class=class="str">"cmt">// Setting its font size to class="num">12 LotSizeText.SetTextColor(clrWhite); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrWhite LotSize.Create("LotSize", class="num">220, class="num">140, class="num">50, class="num">20); class=class="str">"cmt">// Creating the LotSize with specified dimensions LotSize.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack LotSize.SetBGColor(clrWhiteSmoke); class=class="str">"cmt">// Setting its BG Color to clrWhiteSmoke
面板按钮与下单事件的绑定细节
在 OnInit 里先把手数标签写死成 0.01 并染成黑色,这一步决定了面板加载后默认仓位是最小手数,省得交易者手滑填错。紧接着用 Create("BuyButton",110,180,80,25) 在坐标 (110,180) 画出宽 80 高 25 的买按钮,背景刷 clrLime;卖按钮偏右 100 像素放在 (210,180),背景 clrRed,视觉上红绿分开降低误操作概率。 两个按钮建完必须调 ChartRedraw(0) 才会立刻重绘出来,否则在某些 MT5 版本里面板会延迟到下一次报价才显示,返回 INIT_SUCCEEDED 代表初始化链路没断。 OnDeinit 里挨个 Destroy 掉七个对象(主体、标题栏、标题字、手数标签、手数框、买钮、卖钮),不清理的话切换周期会留下残影对象导致下次加载报错。 真正触发交易的是 OnChartEvent:当 id 等于 CHARTEVENT_OBJECT_CLICK 且 sparam 匹配 "BuyButton",就取当前 SYMBOL_ASK 用 LotSize.GetText() 转 double 开多;匹配 "SellButton" 则取 SYMBOL_BID 开空。外汇和贵金属波动剧烈,这种一键面板下单可能瞬间成交在滑点价位,实盘前务必在策略测试器用历史数据验一遍点击响应。
LotSize.SetText("class="num">0.01"); class=class="str">"cmt">// Setting its text to class="num">0.01 LotSize.SetTextColor(clrBlack); class=class="str">"cmt">// Setting its text class="type">class="kw">color to clrBlack BuyButton.Create("BuyButton", class="num">110, class="num">180, class="num">80, class="num">25); class=class="str">"cmt">// Creating the BuyButton with specified dimensions BuyButton.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack BuyButton.SetText("Buy"); class=class="str">"cmt">// Setting its text to "Buy" BuyButton.SetBGColor(clrLime); class=class="str">"cmt">// Setting its BG Color to clrLime SellButton.Create("SellButton", class="num">210, class="num">180, class="num">80, class="num">25); class=class="str">"cmt">// Creating the SellButton with specified dimensions SellButton.SetBorderColor(clrBlack); class=class="str">"cmt">// Setting its Border Color to clrBlack SellButton.SetText("Sell"); class=class="str">"cmt">// Setting its text to "Sell" SellButton.SetBGColor(clrRed); class=class="str">"cmt">// Setting its BG Color to clrRed ChartRedraw(class="num">0); class=class="str">"cmt">// Redrawing the chart to reflect changes class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// Indicating successful initialization } class="type">void OnDeinit(const class="type">int reason) { MainDashboardBody.Destroy(); class=class="str">"cmt">// Destroying the MainDashboardBody object TitleBar.Destroy(); class=class="str">"cmt">// Destroying the TitleBar object TitleText.Destroy(); class=class="str">"cmt">// Destroying the TitleText object LotSizeText.Destroy(); class=class="str">"cmt">// Destroying the LotSizeText object LotSize.Destroy(); class=class="str">"cmt">// Destroying the LotSize object BuyButton.Destroy(); class=class="str">"cmt">// Destroying the BuyButton object SellButton.Destroy(); class=class="str">"cmt">// Destroying the SellButton object } class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { if(id == CHARTEVENT_OBJECT_CLICK) { class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(sparam == "BuyButton") { trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, (class="type">class="kw">double)LotSize.GetText(), ask, class="num">0, class="num">0); } if(sparam == "SellButton") {
◍ 用图表按钮直接发单的底层调用
在 MT5 里把交易按钮做进图表,核心不是画对象,而是接住 OnChartEvent 里的 CHARTEVENT_OBJECT_CLICK。 当 sparam 等于你给按钮起的名字(如 BuyButton / SellButton),就能在点击事件里读实时报价并下单。下面这段把多空两条路径都接上了,注意买用 ask、卖用 bid,滑点和止损先填 0 留给后续逻辑。 外汇与贵金属杠杆高,市价单在流动性薄的时段可能滑出预期,按钮下单前务必确认 LotSize 文本框已取到合法数值。 把代码丢进 EA 的 OnChartEvent,再配合前面创建的 Edit 控件读手数,就能在图上点一下直接开仓,省掉手工敲交易面板的步骤。
class="macro">#include <Trade/Trade.mqh> CTrade trade; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Chart event handling function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { if(id == CHARTEVENT_OBJECT_CLICK) { class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(sparam == "BuyButton") { trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, (class="type">class="kw">double)LotSize.GetText(), ask, class="num">0, class="num">0); } if(sparam == "SellButton") { trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, (class="type">class="kw">double)LotSize.GetText(), bid, class="num">0, class="num">0); } } }
「用中心元素带动整块仪表板」
让静态仪表板整体可移动,最笨的办法是给每个子元素发 CustomChartEvent 去跟随主动元素,但实测下来事件广播太慢,拖拽时帧率会明显掉,基本不可用。 折中方案是把一个元素设成“中心元素”(标题栏就很合适),鼠标只驱动它,其余元素按事先算好的相对偏移同步走。这样每帧只算一次主动位移,再批量套用偏移,效率比事件链高出一个量级。 关键陷阱在距离记录:若每次鼠标移动都重新读取其它元素的 XDis/YDis 再反推偏移,函数会又臭又慢。正确做法是“绑定中心元素时”就存好中心与其它元素的 X/Y 间距常量,之后只更新中心位置、其余元素照常量平移。 下面这段代码是改造前的 OnEvent 单机响应骨架,只处理了单个 RectangleLabel 的按下判定,还没引入整体偏移逻辑——但你能直接抄进 MT5 看拖拽热区是不是按 XDistance+200、YDistance+200 的初值命中。外汇与贵金属图表上跑这类 UI 需注意:高频重绘在低点差时段可能占用主图资源,存在卡顿风险。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> RectangleLabel::OnEvent(<span class="keyword">class="type">int</span> id, <span class="keyword">class="type">long</span> lparam, <span class="keyword">class="type">class="kw">double</span> dparam, <span class="keyword">class="type">class="kw">string</span> sparam) { <span class="comment">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</span> <span class="keyword">if</span>(id == <span class="macro">CHARTEVENT_MOUSE_MOVE</span>) { <span class="comment">class=class="str">"cmt">//define X, Y, XDistance, YDistance, XSize, YSize</span> <span class="keyword">class="type">int</span> X = (<span class="keyword">class="type">int</span>)lparam; <span class="keyword">class="type">int</span> Y = (<span class="keyword">class="type">int</span>)dparam; <span class="keyword">class="type">int</span> MouseState = (<span class="keyword">class="type">int</span>)sparam; <span class="keyword">class="type">class="kw">string</span> name = Name; <span class="keyword">class="type">int</span> XDistance = (<span class="keyword">class="type">int</span>)<span class="functions">ObjectGetInteger</span>(<span class="number">class="num">0</span>, name, <span class="macro">OBJPROP_XDISTANCE</span>); <span class="comment">class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit()</span> <span class="keyword">class="type">int</span> YDistance = (<span class="keyword">class="type">int</span>)<span class="functions">ObjectGetInteger</span>(<span class="number">class="num">0</span>, name, <span class="macro">OBJPROP_YDISTANCE</span>); <span class="comment">class=class="str">"cmt">//Should be class="num">100 initially as we set it in OnInit()</span> <span class="keyword">class="type">int</span> XSize = (<span class="keyword">class="type">int</span>)<span class="functions">ObjectGetInteger</span>(<span class="number">class="num">0</span>, name, <span class="macro">OBJPROP_XSIZE</span>); <span class="comment">class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit()</span> <span class="keyword">class="type">int</span> YSize = (<span class="keyword">class="type">int</span>)<span class="functions">ObjectGetInteger</span>(<span class="number">class="num">0</span>, name, <span class="macro">OBJPROP_YSIZE</span>); <span class="comment">class=class="str">"cmt">//Should be class="num">200 initially as we set it in OnInit()</span> <span class="keyword">if</span>(previousMouseState == <span class="number">class="num">0</span> && MouseState == <span class="number">class="num">1</span>) <span class="comment">class=class="str">"cmt">//Check if this was the MLB first click</span> { mlbDownX = X; <span class="comment">class=class="str">"cmt">//Set mlbDownX(Variable that stores the initial MLB X location) equal to the current X</span> mlbDownY = Y; <span class="comment">class=class="str">"cmt">//Set mlbDownY(Variable that stores the initial MLB Y location) equal to the current Y</span> mlbDownXDistance = XDistance; <span class="comment">class=class="str">"cmt">//Set mlbDownXDistance(Variable that stores the initial XDistance i.e. Width of the dashboard) equal to the current XDistance</span> mlbDownYDistance = YDistance; <span class="comment">class=class="str">"cmt">//Set mlbDownYDistance(Variable that stores the initial YDistance i.e. Height of the dashboard) equal to the current YDistance</span> <span class="keyword">if</span>(X >= XDistance && X <= XDistance + XSize && Y >= YDistance && Y <= YDistance + YSize) <span class="comment">class=class="str">"cmt">//Check if the click was on the dashboard</span> { movingState = <span class="macro">true</span>; <span class="comment">class=class="str">"cmt">//If yes the set movingState to True</span> } } <span class="keyword">if</span>(movingState)<span class="comment">class=class="str">"cmt">//if movingState is true, Update the Dashboard position</span> {
拖拽对象时锁死图表滚动
在中键未按下时,图表默认允许鼠标滚轮平移,这会干扰手动拖拽标注对象的体验。解决方式是在左键按下拖动期间,用 ChartSetInteger(0, CHART_MOUSE_SCROLL, false) 临时关闭滚动,松手后再设回 true。 核心拖动逻辑依赖三行 ObjectSetInteger:以按下瞬间的 XDISTANCE/YDISTANCE 为基准,加上当前光标与按下点的偏移量 (X - mlbDownX) 和 (Y - mlbDownY),实时改写对象坐标。每次更新后调 ChartRedraw(0) 强制重绘,否则画面会滞后于鼠标。 MouseState 回零即判定拖拽结束,movingState 置 false 并恢复图表滚动权限。previousMouseState 在末尾更新,供下一帧比对边沿状态——这套状态机在 MT5 里跑起来,你能直接拖着自建的箭头或文本框满屏走而不触发误平移。外汇与贵金属品种波动剧烈,此类自定义交互脚本仅用于提升盯盘效率,不预示任何方向。
ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, 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) 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 = false;class=class="str">"cmt">//set movingState again to 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 } } ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX);class=class="str">"cmt">//Update XDistance to: mlbDownXDistance + (X - mlbDownX) ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY);class=class="str">"cmt">//Update YDistance to: mlbDownYDistance + (Y - mlbDownY)
◍ 用 Add 把附属元素绑死在标题栏上
静态仪表板要整体拖动,核心是把中心元素(TitleBar)与其余控件的相对偏移记下来。只需存两类信息:附属元素名称,以及它与中心元素在 X、Y 方向上的距离差。 我们在 RectangleLabel 类里加一个私有数组 addedNames、addedXDisDiffrence、addedYDisDiffrence,均不对外暴露。Add 函数负责把数组扩容 1,并在末位写入名称与差值——差值用中心元素 XDISTANCE 减目标元素 XDISTANCE 得到,Y 方向同理。 OnEvent 里要在中心元素位置刷新后跑一个循环:对每个附属名,用新的中心 X/Y 距离减去记录的差值,再写回 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE。这样拖标题栏时,其余元素会保持原间距跟随。 最后在 OnChartEvent() 调用 TitleBar.Add("某元素") 把控件挂上去,并触发 OnEvent;不挂这一环,拖动逻辑就是空转。外汇与贵金属图表上做这类 UI 改动属高风险自定义,建议先在模拟账户 MT5 验证偏移计算。
class="type">class="kw">string addedNamed[]; class="type">int addedXDisDiffrence[], addedYDisDiffrence[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Method to add an object by name to the rectangle label | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void RectangleLabel::Add(class="type">class="kw">string name) { ArrayResize(addedNames, ArraySize(addedNames) + class="num">1); ArrayResize(addedXDisDiffrence, ArraySize(addedXDisDiffrence) + class="num">1); ArrayResize(addedYDisDiffrence, ArraySize(addedYDisDiffrence) + class="num">1); addedNames[ArraySize(addedNames) - class="num">1] = name; addedXDisDiffrence[ArraySize(addedXDisDiffrence) - class="num">1] = ObjectGetInteger(class="num">0, _name, OBJPROP_XDISTANCE) - ObjectGetInteger(class="num">0, name, OBJPROP_XDISTANCE); addedYDisDiffrence[ArraySize(addedYDisDiffrence) - class="num">1] = ObjectGetInteger(class="num">0, _name, OBJPROP_YDISTANCE) - ObjectGetInteger(class="num">0, name, OBJPROP_YDISTANCE); } class=class="str">"cmt">//+------------------------------------------------------------------+ for(class="type">int i = class="num">0; i < ArraySize(addedNames); i++) { ObjectSetInteger(class="num">0, addedNames[i], OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX - addedXDisDiffrence[i]); ObjectSetInteger(class="num">0, addedNames[i], OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY - addedYDisDiffrence[i]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event handling for mouse movements | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void RectangleLabel::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">// Handle mouse movement events for dragging the rectangle label if(id == CHARTEVENT_MOUSE_MOVE) { 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);
「拖拽面板时让附属控件同步位移」
在 MT5 自定义面板里,标题栏被鼠标左键按下且坐标落在对象包围盒内(X 在 XDistance 到 XDistance+XSize、Y 在 YDistance 到 YDistance+YSize 之间)时,movingState 置为 true,这时先关掉 CHART_MOUSE_SCROLL 避免图表乱滚。 随后主对象按鼠标偏移量重设 XDISTANCE / YDISTANCE,并立刻 ChartRedraw 重绘。关键点在于:用 for 循环把 addedNames 数组里每个附属对象也按相同偏移减去各自差值(addedXDisDiffrence / addedYDisDiffrence)同步移动,否则只有标题栏飞、按钮留在原地。 鼠标松开(MouseState==0)就把 movingState 复位、重新打开图表滚轮缩放,并把 previousMouseState 更新为当前值,下一次点击才能正确判断「刚按下」的边沿。 最后把 MainDashboardBody、TitleText、LotSizeText、LotSize、BuyButton、SellButton 全通过 TitleBar.Add 挂到标题栏下,再统一由 TitleBar.OnEvent 分发事件——这样拖标题栏,整块面板带着按钮一起走。外汇与贵金属标的波动剧烈,这类自定义面板仅作辅助,实操请先在模拟盘验证。
class="type">int YDistance = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_YDISTANCE); class="type">int XSize = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_XSIZE); class="type">int YSize = (class="type">int)ObjectGetInteger(class="num">0, name, OBJPROP_YSIZE); 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, false); ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX); ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY); for(class="type">int i = class="num">0; i < ArraySize(addedNames); i++) { ObjectSetInteger(class="num">0, addedNames[i], OBJPROP_XDISTANCE, mlbDownXDistance + X - mlbDownX - addedXDisDiffrence[i]); ObjectSetInteger(class="num">0, addedNames[i], OBJPROP_YDISTANCE, mlbDownYDistance + Y - mlbDownY - addedYDisDiffrence[i]); } ChartRedraw(class="num">0); } if(MouseState == class="num">0) { movingState = false; ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); } previousMouseState = MouseState; } class=class="str">"cmt">// Add the other elements to the Central Element i.e. TitleBar object in this case TitleBar.Add("MainDashboardBody"); TitleBar.Add("TitleText"); TitleBar.Add("LotSizeText"); TitleBar.Add("LotSize"); TitleBar.Add("BuyButton"); TitleBar.Add("SellButton"); class=class="str">"cmt">// Passes events to the TitleBar object TitleBar.OnEvent(id, lparam, dparam, sparam);
别急着下结论
这套可移动 GUI 系列走到第 3 篇,核心交付物是 SimpleTradingEA.mq5(4.3 KB)加四个界面类头文件,合计约 17 KB 代码,把拖拽面板真正落到了 MT5 图表上。读者在讨论区反馈,不会写代码的人也能改改就用,但加带 ± 号的输入框还得自己补 Edit.mqh 的逻辑。 外汇和贵金属市场高杠杆、滑点频繁,这类 GUI 只是降低操作摩擦,不解决策略胜率问题,实盘前务必在策略测试器跑历史回放。 作者留了 CodeBase 和文章链接,下一篇写什么由社区建议驱动;你想改面板布局,直接开 MT5 把 RectangleLabel.mqh 里的坐标参数调一调最实惠。