在 MQL5 中创建交易管理员面板(第一部分):构建消息接口·进阶篇
🛰️

在 MQL5 中创建交易管理员面板(第一部分):构建消息接口·进阶篇

(2/3)· 越过基础概念,直接拆解 GUI 坐标、库文件封装与消息接口的落地路径

含代码示例 第 2/3 篇

很多开发者在 MetaTrader 5 里画面板时凭感觉拖坐标,结果元素在不同图表模板下错位、点不到按钮。更常见的是信号跑出来了,却卡在怎么把确认发到 Telegram 这一步,只能手动复制粘贴。本篇接上篇概念,继续把管理员面板的骨架和通讯神经搭起来。

把图表事件接进自定义面板

在 MT5 的 EA 或指标里,OnChartEvent 是捕捉用户交互的总入口。上面这段 switch 结构把鼠标移动、对象创建删除、图表点击,以及具体按钮的点击都分派到了不同处理函数,你可以直接拷进自己的面板工程里改。 鼠标移动事件里 lparam 和 dparam 分别是 x、y 像素坐标,Print 出来能在专家日志看到类似 Mouse move: x=243 y=118 的数值,用来调试悬浮定位很直观。对象创建和删除事件则通过 sparam 拿到对象名,方便追踪用户在图上画了什么线。 按钮点击这块值得细看:sparam 等于 "SendButton"、"QuickMessageButton"、"MinimizeButton"、"CloseButton" 时各自触发不同逻辑。最小化按钮用 static bool minimized 记住状态,在 500×400 和 500×30 两种尺寸间切换——30 像素高基本只留标题栏,算是一种轻量收起方案。 下方四个处理函数承接了上面的点击:发消息读 inputBox.Text(),失败就在日志写 Failed;最小化切换面板尺寸;关闭直接 Destroy。外汇和贵金属图表上挂这种面板属于高频交互,注意 MT5 对象名冲突可能导致 sparam 对不上,建议在测试账户先跑通再上实盘,相关操作高风险。

MQL5 / C++
      Print("Mouse move: x=", lparam, "  y=", dparam);
      class="kw">break;
      case CHARTEVENT_OBJECT_CREATE:
        Print("Created object: ", sparam);
        class="kw">break;
      case CHARTEVENT_OBJECT_DELETE:
        Print("Deleted object: ", sparam);
        class="kw">break;
      case CHARTEVENT_CLICK:
        Print("Mouse click on chart: x=", lparam, "  y=", dparam);
        class="kw">break;
      case CHARTEVENT_OBJECT_CLICK:
        if (sparam == "SendButton")
        {
          OnSendButtonClick();
        }
        else if (sparam == "QuickMessageButton")
        {
          OnQuickMessageButtonClick();
        }
        else if (sparam == "MinimizeButton")
        {
          OnMinimizeButtonClick();
        }
        else if (sparam == "CloseButton")
        {
          OnCloseButtonClick();
        }
        class="kw">break;
  }
}
class="type">void OnSendButtonClick()
{
  class="type">class="kw">string customMessage = inputBox.Text();
  if (SendMessageToTelegram(customMessage))
  {
    Print("Message sent: ", customMessage);
  }
  else
  {
    Print("Failed to send message.");
  }
}
class="type">void OnQuickMessageButtonClick()
{
  if (SendMessageToTelegram(QuickMessage))
  {
    Print("Quick message sent: ", QuickMessage);
  }
  else
  {
    Print("Failed to send quick message.");
  }
}
class="type">void OnMinimizeButtonClick()
{
  class="kw">static class="type">bool minimized = false;
  if (minimized)
  {
    adminPanel.SetSize(class="num">500, class="num">400);
    minimized = false;
  }
  else
  {
    adminPanel.SetSize(class="num">500, class="num">30);
    minimized = true;
  }
}
class="type">void OnCloseButtonClick()
{
  adminPanel.Destroy();

「收尾时给面板一句交代」

在 MT5 的 EA 或脚本里,面板资源释放后若什么都不打印,调试阶段很难判断是正常关闭还是异常退出。 一行 Print 把状态吐到日志,是低成本的自检手段。 外汇与贵金属品种波动剧烈,面板频繁开关时,这种日志能帮助快速定位是用户手动关闭还是断线重连触发的销毁。

MQL5 / C++
  Print("Panel closed.");
}

◍ 用 WebRequest 把 MT5 信号推到 Telegram

在 MT5 里做自动喊单或风控播报,核心是把消息从 EA/脚本经 Telegram Bot API 发出去。实现上先硬编码 botToken 与 chatId——前者是机器人鉴权凭证,后者决定消息落到哪个会话,两者错填就会发到未知地址或鉴权失败。 请求体必须按 API 要求拼成 JSON:把 chat_id 和 text 字段连成 {"chat_id":"...", "text":"..."} 字符串,再用 StringToCharArray 转成 char 数组并 ArrayResize 对齐长度,否则 WebRequest 传参会截断。 timeout 建议设 5000 毫秒,这是等 Telegram 服务器回应的上限;res 数组收响应体,responseHeaders 收头信息,方便排查。WebRequest 发 POST,若返回 200 即成功,非 200 则打印 HTTP 码与 GetLastError(),这一步的错误处理直接决定推送功能是否可调试。 下面这段整合代码可直接贴进 MT5 测:把 BOT TOKEN 和 CHAT ID 换成你自己的,EA 里调用即可在终端日志看到发送结果。外汇与贵金属波动剧烈,自动推送仅作辅助通知,不构成任何交易方向建议,实盘前务必在模拟盘验证机器人连通性。

MQL5 / C++
class="type">class="kw">string botToken = "BOT TOKEN";
class="type">class="kw">string chatId = "CHAT ID";
class="type">class="kw">string url = "https:class=class="str">"cmt">//api.telegram.org/bot" + botToken + "/sendMessage";
class="type">char post_data[];
class="type">class="kw">string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}";
ArrayResize(post_data, StringToCharArray(jsonMessage, post_data));
class="type">int timeout = class="num">5000;
class="type">char result[];
class="type">class="kw">string responseHeaders;
class="type">int res = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, post_data, result, responseHeaders);
if (res == class="num">200) class=class="str">"cmt">// HTTP class="num">200 OK
{
   Print("Message sent successfully: ", message);
   class="kw">return true;
}
else
{
   Print("Failed to send message. HTTP code: ", res, " Error code: ", GetLastError());
   Print("Response: ", CharArrayToString(result));
   class="kw">return false;
}

拖拽与尺寸更新的事件分支

在 MT5 自定义面板里,鼠标交互靠图表事件驱动。下面这段处理逻辑把「拖动中」「点中对象」「空白处点击」三种情况分开走,避免面板乱跳。 HandleDragging 接收 id、lparam、dparam:当 id 为 CHARTEVENT_MOUSE_MOVE 且 m_Dragging 为真,用 lparam 减偏移算出新坐标并调用 SetXPosition 只改位置不改尺寸;当 id 为 CHARTEVENT_OBJECT_CLICK,记录按下点偏移并把 m_Dragging 置真;当 id 为 CHARTEVENT_CLICK(图表空白),把 m_Dragging 置假,拖动结束。 SetSize 改 m_width、m_height 后调 UpdateDialogSize;后者用 ObjectSetInteger 把面板锚定左上角(OBJPROP_CORNER=0),并写入 OBJPROP_XSIZE / OBJPROP_YSIZE 实际像素。SetXPosition 则只动 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE,随后再调一次 UpdateDialogSize 保尺寸同步。 外汇与贵金属图表加载这类面板属高风险环境,事件坐标基于当前图表分辨率,换屏或缩放后偏移量可能偏差数像素,建议在 MT5 里改 m_OffsetX/Y 初始值实测。

MQL5 / C++
class="type">void HandleDragging(const class="type">int id, const class="type">long lparam, const class="type">class="kw">double dparam)
{
   if (id == CHARTEVENT_MOUSE_MOVE && m_Dragging)
   {
      class="type">int new_x = (class="type">int)lparam - m_OffsetX;
      class="type">int new_y = (class="type">int)dparam - m_OffsetY;
      SetXPosition(new_x, new_y); class=class="str">"cmt">// Update the position without changing size
   }
   else if (id == CHARTEVENT_OBJECT_CLICK)
   {
      m_OffsetX = (class="type">int)lparam;
      m_OffsetY = (class="type">int)dparam;
      m_Dragging = true;
   }
   else if (id == CHARTEVENT_CLICK)
   {
      m_Dragging = false;
   }
}
class="type">void SetSize(class="type">int width, class="type">int height)
{
   m_width = width;
   m_height = height;
   class=class="str">"cmt">// Call the method to update the size of the dialog
   UpdateDialogSize();
}
class="type">void UpdateDialogSize()
{
   class=class="str">"cmt">// Adjust the internal layout or size of the controls within the dialog here
   class=class="str">"cmt">// Example: Resize or reposition child controls based on the new dimensions
   class=class="str">"cmt">// Ensure that dialog dimensions are respected within its design
   ObjectSetInteger(ChartID(), Name(), OBJPROP_CORNER, class="num">0); class=class="str">"cmt">// This aligns the dialog to the chart corner
   ObjectSetInteger(ChartID(), Name(), OBJPROP_XSIZE, m_width);   class=class="str">"cmt">// Width of the dialog
   ObjectSetInteger(ChartID(), Name(), OBJPROP_YSIZE, m_height); class=class="str">"cmt">// Height of the dialog
}
class="type">void SetXPosition(class="type">int x, class="type">int y)
{
   class=class="str">"cmt">// Set the X and Y positions of the dialog panel
   ObjectSetInteger(ChartID(), Name(), OBJPROP_XDISTANCE, x);
   ObjectSetInteger(ChartID(), Name(), OBJPROP_YDISTANCE, y);
   class=class="str">"cmt">// Call the method to update the size of the dialog
   UpdateDialogSize();
}

「在 OnInit 里把管理面板控件逐个挂上去」

EA 启动阶段最务实的一件事,就是把图形界面先立起来。下面这段初始化函数把对话框和五个子控件按坐标铺好,任何一步 Create 失败就直接返回 INIT_FAILED,MT5 会拒绝加载 EA,不会留一个半残面板给你。 面板主体 adminPanel 建在图表左上角偏移 (30,30),宽 500 高 400;输入框 inputBox 嵌在面板内 (5,5) 起手,宽 460 高 50,够塞一整条自定义指令。发送按钮落在 (270,50),尺寸 460x80,文字写死 "Send Message";快速消息按钮在 (180,200) 占 350x230,标成 "Invalid Signal",用来一键广播“信号无效”这类预警。 最小化与关闭按钮的坐标有点反直觉:minimizeButton 的 y 用了 -22,closeButton 的 x 写成 +435、y 也是 -22,本质是贴着面板标题栏右侧放。最后 adminPanel.Show() 把整块推出去,并用 ChartSetInteger 打开 OBJECT_CREATE / OBJECT_DELETE 事件,后续控件交互才收得到消息。外汇与贵金属波动剧烈,这类面板只做人工确认通道,不参与自动下单决策,实盘前务必在策略测试器里先跑通创建流程。

MQL5 / C++
class="type">int OnInit()
{
   class="type">long chart_id = ChartID();
   class=class="str">"cmt">// Create the dialog
   if (!adminPanel.Create(chart_id, "Admin Panel", class="num">0, class="num">30, class="num">30, class="num">500, class="num">400))
   {
      Print("Failed to create dialog");
      class="kw">return INIT_FAILED;
   }
   class=class="str">"cmt">// Create the input box
   if (!inputBox.Create(chart_id, "InputBox", class="num">0, class="num">5, class="num">5, class="num">460,class="num">50 ))
   {
      Print("Failed to create input box");
      class="kw">return INIT_FAILED;
   }
   adminPanel.Add(inputBox);
   class=class="str">"cmt">// Create the send button for custom messages
   if (!sendButton.Create(chart_id, "SendButton", class="num">0, class="num">270, class="num">50, class="num">460, class="num">80))
   {
      Print("Failed to create send button");
      class="kw">return INIT_FAILED;
   }
   sendButton.Text("Send Message");
   adminPanel.Add(sendButton);
   class=class="str">"cmt">// Create the quick message button
   if (!quickMessageButton.Create(chart_id, "QuickMessageButton", class="num">0, class="num">180, class="num">200, class="num">350, class="num">230))
   {
      Print("Failed to create quick message button");
      class="kw">return INIT_FAILED;
   }
   quickMessageButton.Text("Invalid Signal");
   adminPanel.Add(quickMessageButton);
   class=class="str">"cmt">// Create the minimize button
   if (!minimizeButton.Create(chart_id, "MinimizeButton",class="num">0, class="num">405, -class="num">22, class="num">435, class="num">0))
   {
      Print("Failed to create minimize button");
      class="kw">return INIT_FAILED;
   }
   minimizeButton.Text("_");
   adminPanel.Add(minimizeButton);
   class=class="str">"cmt">// Create the close button
   if (!closeButton.Create(chart_id, "CloseButton",class="num">0 , +class="num">435, -class="num">22,+class="num">465,class="num">0))
   {
      Print("Failed to create close button");
      class="kw">return INIT_FAILED;
   }
   closeButton.Text("X");
   adminPanel.Add(closeButton);
   adminPanel.Show();
   class=class="str">"cmt">// Enable chart events
   ChartSetInteger(ChartID(), CHART_EVENT_OBJECT_CREATE, true);
   ChartSetInteger(ChartID(), CHART_EVENT_OBJECT_DELETE, true);

◍ 把面板事件接进图表生命周期

EA 初始化收尾时,先打开鼠标滚轮事件开关再强制重绘,是保证自定义面板能实时响应交互的前提。下面这三行若漏掉 ChartRedraw,面板在加载瞬间可能停留在空白帧,要等下一次报价才刷出来。 ChartSetInteger(ChartID(), CHART_EVENT_MOUSE_WHEEL, true); ChartRedraw(); Print("Initialization complete"); return INIT_SUCCEEDED; 反初始化里只做两件事:销毁面板对象并打印退出日志。adminPanel.Destroy() 必须放在 Print 前,否则对象句柄残留可能导致 MT5 在切换周期时报 'object already exists' 类警告。 图表事件分发是面板能用的核心。OnChartEvent 里先交给我们自己的 HandleDragging 处理拖拽,再用 switch 按 id 分流:按键、鼠标移动、对象增删、图表点击都会进 Print 留痕,方便你开专家日志抓交互坐标。 对象点击分支是按 sparam 名字判定的。SendButton、QuickMessageButton、MinimizeButton 分别对应发消息、快捷消息、最小化——你在 MT5 里改了按钮的 OBJPROP_NAME 就必须同步改这里的字符串,否则点击无响应还不会报错,这种静默失效在外汇高频复盘时很误事,贵金属跳空时段尤甚。

MQL5 / C++
ChartSetInteger(ChartID(), CHART_EVENT_MOUSE_WHEEL, true);
ChartRedraw();
Print("Initialization complete");
class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
   adminPanel.Destroy();
   Print("Deinitialization complete");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert 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)
{
   class=class="str">"cmt">// Handle dragging and other events
   adminPanel.HandleDragging(id, lparam, dparam);
   class=class="str">"cmt">// Handle different types of events
   class="kw">switch(id)
   {
      case CHARTEVENT_KEYDOWN:
         Print("Key pressed: code=", lparam);
         class="kw">break;
      
      case CHARTEVENT_MOUSE_MOVE:
         Print("Mouse move: x=", lparam, "  y=", dparam);
         class="kw">break;
      case CHARTEVENT_OBJECT_CREATE:
         Print("Created object: ", sparam);
         class="kw">break;
      case CHARTEVENT_OBJECT_DELETE:
         Print("Deleted object: ", sparam);
         class="kw">break;
      case CHARTEVENT_CLICK:
         Print("Mouse click on chart: x=", lparam, "  y=", dparam);
         class="kw">break;
      case CHARTEVENT_OBJECT_CLICK:
         if (sparam == "SendButton")
         {
            OnSendButtonClick();
         }
         else if (sparam == "QuickMessageButton")
         {
            OnQuickMessageButtonClick();
         }
         else if (sparam == "MinimizeButton")
         {
把信号确认流交给小布盯盘
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到面板状态与消息回执,把重复劳动交给小布,你专注决策。

常见问题

原点在图表窗口左上角 (0,0),用左上和右下两点定义元素;不同模板 DPI 与坐标轴偏移不同,未做相对换算就会错位。
小布盯盘内置了品种页的消息诊断与回执视图,可承接类似信号确认流,减少你在 MT5 和通讯软件间来回切换。
倾向放在加密的 ini 或全局变量而非硬编码源码,EA 重载时读取,避免明文随文件分发泄露。
把坐标绘制、消息发送拆成独立 mqh,多 EA 复用且改界面不影响通讯逻辑,编译也更快。
需测空信号、网络超时、Telegram 限流三种情况,概率上这类异常比正常路径更容易让面板卡死。