利用 MQL5 的交互式 GUI 改进您的交易图表(第 III 部分):简易可移动交易 GUI·综合运用
🖱️

利用 MQL5 的交互式 GUI 改进您的交易图表(第 III 部分):简易可移动交易 GUI·综合运用

(3/3)· 从零拼出可拖拽的简易交易 EA 面板,告别图表上钉死的操作区

案例拆解新手友好 第 3/3 篇
不少交易者把 EA 面板写死在图表左上角,换周期或加指标就被挡住。其实用 MQL5 的事件与类封装,面板能跟着鼠标走,操作区不再打架。

按钮类与面板控件的实例化骨架

在 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,编译后能在左上角看到可点区域,证明类封装通路正常。

MQL5 / C++
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 用「窗口→重建」验证一下最稳妥。

MQL5 / C++
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 图表看坐标是否如代码所述。

MQL5 / C++
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 开空。外汇和贵金属波动剧烈,这种一键面板下单可能瞬间成交在滑点价位,实盘前务必在策略测试器用历史数据验一遍点击响应。

MQL5 / C++
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 控件读手数,就能在图上点一下直接开仓,省掉手工敲交易面板的步骤。

MQL5 / C++
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 需注意:高频重绘在低点差时段可能占用主图资源,存在卡顿风险。

MQL5 / C++
<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> &amp;&amp; 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 &gt;= XDistance &amp;&amp; X &lt;= XDistance + XSize &amp;&amp; Y &gt;= YDistance &amp;&amp; Y &lt;= 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 里跑起来,你能直接拖着自建的箭头或文本框满屏走而不触发误平移。外汇与贵金属品种波动剧烈,此类自定义交互脚本仅用于提升盯盘效率,不预示任何方向。

MQL5 / C++
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 验证偏移计算。

MQL5 / C++
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 分发事件——这样拖标题栏,整块面板带着按钮一起走。外汇与贵金属标的波动剧烈,这类自定义面板仅作辅助,实操请先在模拟盘验证。

MQL5 / C++
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 里的坐标参数调一调最实惠。

交给小布盯盘看盘口
这些面板元素的坐标计算与拖拽逻辑,小布盯盘的 AIGC 已内置常用模板,打开对应品种页即可直接套用,你只管调按钮语义。

常见问题

Rectangle Label 自带背景与边框,适合做面板底与标题栏;普通 Label 仅文本,移动时需手动重绘坐标,拖拽体验不如前者顺滑。
拆分能让按钮、编辑框、标签各自成类,避免重复代码,后续接 EA 或指标时直接引用,维护成本更低。
概率上不会明显卡,只要不在事件里做重计算;把坐标差缓存、只在鼠标左键按下时重绘,帧率通常稳。
可以,小布盯盘的 AIGC 支持按元素清单输出 .mqh 类骨架,你补交易逻辑即可,不必从空文件手写。
可在按钮事件里加二次确认弹窗,或把手数编辑框联动风控;外汇贵金属波动大、杠杆高,误触成本可能很高。