在MQL5中创建交易管理员面板(第十一部分):现代化功能通信接口(1)·进阶篇
📘

在MQL5中创建交易管理员面板(第十一部分):现代化功能通信接口(1)·进阶篇

第 2/2 篇

把通信面板拆成可复用模块

做管理员面板迁移时,先别急着画新界面,得把交易员和管理员之间的通信需求理清楚。单向发指令靠快捷按钮省时间,但自定义消息输入框不能少;某些场景允许交易员回帖才构成双向,Telegram 的聊天 ID 能区分一对一、频道和群组,联系人存储得按个人和群组分类;截图分享现在几乎是标配,MQL5 终端自带截图工具,加个按钮直接挂到消息里最实用。 代码组织上,把每个面板的逻辑隔离进独立头文件(定义、枚举、类声明、方法实现全塞进去),主 EA 只调接口不管 GUI 细节。CommunicationsDialog.mqh 就是这么干的:顶部用 #ifndef 防重复包含,引 Dialog.mqh、Panel.mqh、Edit.mqh、Button.mqh、Label.mqh、VirtualKeys.mqh 以及自写 Telegram.mqh;宏 COMM_PANEL_LEFT=80、UI_INDENT=10、UI_GAP=5 管几何间距;两个枚举切消息类型(OUTGOING/INCOMING/SYSTEM)和状态(SENT/FAILED)。 类里私有成员放面板、按钮、标签、编辑框和深色模式标志;公有方法覆盖构造、InitCredentials、CreatePanel、OnEvent、OnSendMessage;私有辅助管侧边栏、主区、撰写区、主题、历史读写和格式化。CreatePanel 按顺序建侧边栏→主消息区→撰写区,任一步失败立刻终止,全建完才 ApplyTheme 并重绘。 截图按钮走 OnCaptureScreenshot,把当前图表存成带时间戳的 GIF 进 MQL5 Files 文件夹,并写一条系统消息标记成败。OnSendMessage 先校验空内容和凭证,调 Telegram 发送,历史文件记一笔,对话区追加带勾或叉的消息,最后输入框复位占位符。 主 EA(New_Admin_Pane.mq5)里用 HandleCommunications 接通信按钮:首次点击才 new 实例、拿存储的 chatID 和 token 初始化并 CreatePanel,之后只切可见性。这种懒加载把资源占用压到最低,界面和 EA 其他部分也解耦干净。外汇与贵金属交易自带高杠杆风险,这类面板只解决通信效率,不替你判断进场。

MQL5 / C++
class="macro">#ifndef COMMUNICATIONS_MQH
class="macro">#define COMMUNICATIONS_MQH
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\Panel.mqh>
class="macro">#include <Controls\Edit.mqh>
class="macro">#include <Controls\Button.mqh>
class="macro">#include <Controls\Label.mqh>
class="macro">#include <VirtualKeys.mqh>
class="macro">#include "Telegram.mqh"
class="macro">#define COMM_PANEL_LEFT    class="num">80
…
enum ENUM_UI_CONSTANTS { UI_INDENT=class="num">10, UI_GAP=class="num">5, … };
enum ENUM_MESSAGE_TYPE   { MSG_OUTGOING, MSG_INCOMING, MSG_SYSTEM };
enum ENUM_MESSAGE_STATUS { STATUS_SENT,   STATUS_FAILED };
class CCommunicationDialog : class="kw">public CAppDialog
{
class="kw">private:
  CPanel  m_mainPanel, m_composePanel;
  CButton m_toggleSidebar, m_sendButton, m_attachButton, m_screenshotButton;
  CLabel  m_usernameLabel, m_chatIdLabel, m_tokenLabel, m_mainUserLabel;
  CEdit   m_usernameInput, m_chatIdInput, m_tokenInput, m_messageThread, m_messageInput;
  class="type">bool    m_isDarkMode;
class="kw">public:
  CCommunicationDialog():m_isDarkMode(false){}
  class="type">bool CreatePanel(...);
  class="kw">virtual class="type">bool OnEvent(...);
  class="type">void OnSendMessage();
  …  
class="kw">private:
  class="type">bool CreateSidebar();
  class="type">bool CreateMainContent();
  class="type">bool CreateComposeArea();
  class="type">void ApplyTheme();
  class="type">void LoadChatHistory(class="type">class="kw">string);
  class="type">void SaveMessageToHistory(...);
  class="type">class="kw">string FormatMessage(...);
  class="type">void AddMessageToHistory(...);
};
class="macro">#endif class=class="str">"cmt">// COMMUNICATIONS_MQH

「自定义对话框的面板拼装逻辑」

在 MT5 里用 CAppDialog 派生类搭交互面板,核心是把各子控件按坐标塞进主窗体。下面这段 CreatePanel 就是总装配函数:先调基类 Create 建出对话框区域,失败直接返 false;随后依次建侧边栏、主内容区、输入区,任一步失败都 Destroy 并退出,避免半残界面留在图上。

MQL5 / C++
class="type">bool CCommunicationDialog::CreatePanel(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,
                                      const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2)
{
  if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(false);
  if(!CreateSidebar())     { Destroy(); class="kw">return(false); }
  if(!CreateMainContent()) { Destroy(); class="kw">return(false); }
  Add(m_mainPanel);
  Add(m_messageThread);
  Add(m_mainUserLabel);
  if(!CreateComposeArea()){ Destroy(); class="kw">return(false); }
  Add(m_composePanel);
  Add(m_messageInput);
  Add(m_screenshotButton);
  Add(m_attachButton);
  Add(m_sendButton);
  ApplyTheme();
  LoadChatHistory(m_chatIdInput.Text());
  ChartRedraw();
  class="kw">return(true);
}
逐行看:第3行基类 Create 接管 chart/name/subwin 和四角坐标;第4–5行侧栏与主区建造失败就整体销毁;第6–8行把消息线程、用户标签加进对话框;第9行输入区失败同样回滚;第10–14行挂上编辑框、截图/附件/发送按钮;第15行套用主题色;第16行按 Chat ID 载入历史;第17行重绘,第18行返回成功。 侧边栏 CreateSidebar 用固定缩进 UI_INDENT 和宽 UI_SIDEBAR_WIDTH 排布,首个控件是 16 像素高的月亮切换钮,其下间隔 UI_GAP 依次放用户名、Chat ID、Bot Token 的输入框。主内容区 CreateMainContent 从 x1=UI_SIDEBAR_WIDTH 起铺到客户区右下减掉输入区高度,消息线程设为只读、9号字、左对齐。
MQL5 / C++
class="type">bool CCommunicationDialog::CreateSidebar()
{
  class="type">int x=UI_INDENT, y=UI_INDENT, w=UI_SIDEBAR_WIDTH-class="num">2*UI_INDENT, h=class="num">16;
  m_toggleSidebar.Create(m_chart_id,m_name+"_Toggle",m_subwin,x,y,x+h,y+h);
  m_toggleSidebar.Text("🌙"); Add(m_toggleSidebar);
  y+=h+UI_GAP;
  m_usernameLabel.Create(...); m_usernameLabel.Text("Username:");
  m_usernameInput.Create(...); Add(m_usernameLabel); Add(m_usernameInput);
  y+=h+UI_GAP;
  m_chatIdLabel.Create(...); m_chatIdLabel.Text("Chat ID:");
  m_chatIdInput.Create(...); Add(m_chatIdLabel); Add(m_chatIdInput);
  y+=h+UI_GAP;
  m_tokenLabel.Create(...); m_tokenLabel.Text("Bot Token:");
  m_tokenInput.Create(...); Add(m_tokenLabel); Add(m_tokenInput);
  class="kw">return(true);
}
这里 h=16 是写死的控件高,若你改 DPI 缩放可能显拥挤,可调 UI_INDENT 或 h 验证。 输入区 CreateComposeArea 贴底布置,y1=客户高-UI_COMPOSE_HEIGHT。按钮文字用 StringFormat("%c",N) 取 ASCII 字符:58 是冒号、49 是‘1’、216 是扩展字符,想换图标直接改数字。截图函数 OnCaptureScreenshot 用 TimeCurrent 打时间戳并去掉冒号,存到 TerminalDataPath 的 MQL5\Files\ 下 GIF,外汇和贵金属行情波动剧烈、截图仅作记录、不构成任何方向暗示。

MQL5 / C++
class="type">bool CCommunicationDialog::CreatePanel(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,
                                      const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2)
{
  if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2)) class="kw">return(false);
  if(!CreateSidebar())     { Destroy(); class="kw">return(false); }
  if(!CreateMainContent()) { Destroy(); class="kw">return(false); }
  Add(m_mainPanel);
  Add(m_messageThread);
  Add(m_mainUserLabel);
  if(!CreateComposeArea()){ Destroy(); class="kw">return(false); }
  Add(m_composePanel);
  Add(m_messageInput);
  Add(m_screenshotButton);
  Add(m_attachButton);
  Add(m_sendButton);
  ApplyTheme();
  LoadChatHistory(m_chatIdInput.Text());
  ChartRedraw();
  class="kw">return(true);
}
class="type">bool CCommunicationDialog::CreateSidebar()
{
  class="type">int x=UI_INDENT, y=UI_INDENT, w=UI_SIDEBAR_WIDTH-class="num">2*UI_INDENT, h=class="num">16;
  m_toggleSidebar.Create(m_chart_id,m_name+"_Toggle",m_subwin,x,y,x+h,y+h);
  m_toggleSidebar.Text("🌙"); Add(m_toggleSidebar);
  y+=h+UI_GAP;
  m_usernameLabel.Create(...); m_usernameLabel.Text("Username:");
  m_usernameInput.Create(...); Add(m_usernameLabel); Add(m_usernameInput);
  y+=h+UI_GAP;
  m_chatIdLabel.Create(...); m_chatIdLabel.Text("Chat ID:");
  m_chatIdInput.Create(...); Add(m_chatIdLabel); Add(m_chatIdInput);
  y+=h+UI_GAP;
  m_tokenLabel.Create(...); m_tokenLabel.Text("Bot Token:");
  m_tokenInput.Create(...); Add(m_tokenLabel); Add(m_tokenInput);
  class="kw">return(true);
}
class="type">bool CCommunicationDialog::CreateMainContent()
{
  class="type">int x1=UI_SIDEBAR_WIDTH, y1=class="num">0;
  class="type">int x2=ClientAreaWidth(), y2=ClientAreaHeight()-UI_COMPOSE_HEIGHT;
  m_mainPanel.Create(...);
  m_messageThread.Create(...);
  m_messageThread.ReadOnly(true);
  m_messageThread.FontSize(class="num">9);
  m_messageThread.TextAlign(ALIGN_LEFT);
  m_mainUserLabel.Create(...);
  m_mainUserLabel.Text(m_usernameInput.Text());
  class="kw">return(true);
}
class="type">bool CCommunicationDialog::CreateComposeArea()
{
  class="type">int caW=ClientAreaWidth(), caH=ClientAreaHeight();
  class="type">int y1=caH-UI_COMPOSE_HEIGHT, y2=caH;
  class="type">int x1=UI_SIDEBAR_WIDTH, x2=caW;
  m_composePanel.Create(...);
  m_screenshotButton.Create(...); m_screenshotButton.Text(StringFormat("%c",class="num">58));
  m_attachButton.    Create(...); m_attachButton.    Text(StringFormat("%c",class="num">49));
  m_sendButton.      Create(...); m_sendButton.      Text(StringFormat("%c",class="num">216));
  m_messageInput.Create(...);
  m_messageInput.Text("Type here...");
  m_messageInput.Color(clrGray);
  class="kw">return(true);
}
class="type">void CCommunicationDialog::OnCaptureScreenshot()
{
  class="type">class="kw">datetime ts=TimeCurrent();
  class="type">class="kw">string tstr=TimeToString(ts, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
  StringReplace(tstr,":","");  
  class="type">class="kw">string path=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\";
  class="type">class="kw">string fn=path+"shot_"+tstr+".gif";

◍ 把图表截图和消息流塞进MT5面板

这段逻辑把「截屏+发Telegram+本地留痕」三件事压在同一个对话框类里。先说截图:ChartScreenShot 用 chart_id 和文件名 fn 调一次,宽高传 0,0 表示抓当前可视区域,返回 bool 决定后面写「Screenshot saved」还是「Screenshot failed」。 发消息前有两道空值闸:输入框是空串或还是占位符 "Type here..." 直接 return;chatId 或 token 任一为空也 return。过了闸才用 TimeCurrent() 打时间戳,调 SendMessageToTelegram,并把结果写进 SaveMessageToHistory,UI 上用 FormatMessage 拼一行带对勾(0x2713)或叉(0x2717)的文本。 ApplyTheme 里暗色背景写死 0x2E2E35,亮色用 clrWhite,文字色反转。FormatMessage 用 StringFormat("[%s] %s: %s %s", 分秒时间, 用户标签/System/Them, 正文, 状态图标)——系统消息不挂图标。 开 MT5 把这段抄进 EA 的对话框类,改 fn 路径就能验证截图是否落盘;外汇和贵金属波动剧烈,这类自动截屏若用于信号分享,务必先确认经纪商允许且自知高风险。

MQL5 / C++
  class="type">bool ok=ChartScreenShot(m_chart_id,fn,class="num">0,class="num">0);
  m_messageThread.Text(
      FormatMessage(ok ? "Screenshot saved: "+fn : "Screenshot failed",
            MSG_SYSTEM, ok ? STATUS_SENT : STATUS_FAILED, ts));
  ChartRedraw();
}
class="type">void CCommunicationDialog::OnSendMessage()
{
  class="type">class="kw">string txt=m_messageInput.Text();
  if(txt==""||txt=="Type here...") { class=class="str">"cmt">/* show error */ class="kw">return; }
  class="type">class="kw">string chatId=m_chatIdInput.Text(), token=m_tokenInput.Text();
  if(chatId==""||token=="")      { class=class="str">"cmt">/* show error */ class="kw">return; }
  class="type">class="kw">datetime ts=TimeCurrent();
  class="type">bool sent = SendMessageToTelegram(txt, chatId, token);
  SaveMessageToHistory(chatId, ts, MSG_OUTGOING,
              sent ? STATUS_SENT : STATUS_FAILED, txt);
  m_messageThread.Text(FormatMessage(txt, MSG_OUTGOING,
                  sent ? STATUS_SENT : STATUS_FAILED, ts));
  m_messageInput.Text("Type here...");
  m_messageInput.Color(clrGray);
  ChartRedraw();
}
class="type">void CCommunicationDialog::ApplyTheme()
{
  class="type">color bg = m_isDarkMode ? (class="type">color)0x2E2E35 : clrWhite;
  class="type">color txt= m_isDarkMode ? clrWhite      : clrBlack;
  m_mainPanel.ColorBackground(bg);
  m_composePanel.ColorBackground(bg);
  m_messageThread.Color(txt);
  m_messageInput.Color(txt);
  …
}
class="type">class="kw">string CCommunicationDialog::FormatMessage(const class="type">class="kw">string message,
                  ENUM_MESSAGE_TYPE type,
                  ENUM_MESSAGE_STATUS status,
                  class="type">class="kw">datetime ts)
{
  class="type">class="kw">string user = (type==MSG_OUTGOING ? m_mainUserLabel.Text()
                : type==MSG_INCOMING ? "Them" : "System");
  class="type">class="kw">string ico = (status==STATUS_SENT ? ShortToString(0x2713)
                : ShortToString(0x2717));
  class="kw">return StringFormat("[%s] %s: %s %s",
          TimeToString(ts,TIME_MINUTES), user, message,
          (type==MSG_SYSTEM ? "" : ico));
}
class=class="str">"cmt">// handle showing/hiding communications panel
class="type">void HandleCommunications()
{
  if(g_commPanel)
  {
    class=class="str">"cmt">// toggle visibility if already created
    if(g_commPanel.IsVisible()) g_commPanel.Hide();

面板首次创建的容错写法

这段逻辑处理的是通信面板在图表上的懒加载:若全局对象已存在就直接调 Show 并重绘图表退出,避免重复构造。 首次运行时走 new CCommunicationDialog() 分支,用 CreatePanel 在子窗口 g_subwin 以坐标 (80,100) 为左上角、宽 430 高 500 像素建立界面;任一参数不对都会返回 false。 创建失败时要先 delete 对象、置 NULL 再 Print 错误码,否则 MT5 会留下悬空指针导致后续 ChartRedraw 崩脚本。 InitCredentials 填入 TwoFactorChatID 与 TwoFactorBotToken 后才 Show,意味着密钥不写死在面板资源里,换账号只改外部变量即可。外汇与贵金属品种波动剧烈,这类自动通知面板仅作辅助,信号延迟或 MT5 断线都可能让消息漏发,实盘前务必在策略测试器外连模拟账号跑一遍。

MQL5 / C++
      else                  g_commPanel.Show();
      ChartRedraw();
      class="kw">return;
   }
   class=class="str">"cmt">// first‐time creation
   g_commPanel = new CCommunicationDialog();
   class=class="str">"cmt">// build the UI
   if(!g_commPanel.CreatePanel(g_chart_id,
                               "CommunicationsPanel",
                               g_subwin,
                               class="num">80, class="num">100, class="num">430, class="num">500))
   {
      class="kw">delete g_commPanel;
      g_commPanel = NULL;
      Print("CommPanel creation failed: ", GetLastError());
      class="kw">return;
   }
   class=class="str">"cmt">// pre‐fill credentials and display
   g_commPanel.InitCredentials(TwoFactorChatID, TwoFactorBotToken);
   g_commPanel.Show();
   ChartRedraw();
}

「面板实测:从编译到发消息跑通了」

在 MT5 里编译这份 EA,零错误零警告,主界面按预期带出切换按钮与「交易 / 通信 / 分析 / 全部显示隐藏」控件,说明框架层没拖后腿。 点「通信」后 CCommunicationDialog 被实例化,分配图表区内正常出对话框,证明 CreatePanel 连同侧边栏、主线程、撰写区都搭对了。凭证我试了两条路:EA 参数里填 TwoFactorChatID / TwoFactorBotToken,以及直接在侧边栏字段改,InitCredentials 都能正确回填并刷新「You」标签。 填完有效凭证,撰写框敲示例消息、回车或点发送,线程里冒出带 ✓ 的消息,Telegram 那边也同步收到——WebRequest 与 JSON 转义是实打实通了。 月亮太阳按钮切主题即时生效,背景与文字色跟着变,还会往对话线程塞一条「已启用深色 / 浅色模式」系统消息。截图键预期往 MQL5\Files 落一个 GIF 并打印确认;附件键目前只打占位「Attachment TBD」,完整上传还没落地,外汇贵金属自动化这类功能上线前务必先在模拟盘验证,接口失联概率不低。

◍ 把这条线请下神坛

眼下这套 EA 消息面板的第一阶段已经能跑通凭证输入、主题设置和 Telegram 转发,但标准库自带的 CEdit 只认单行、还卡字符数,想塞多行聊天记录就得另找社区扩展。文件层面,CommunicationsDialog.mqh 是 34.07 KB 的通信模块,得丢进 MQL5/Include;New_Admin_Panel.mq5 主程序 8.56 KB,All_Modules.zip 17.78 KB 含全部头文件,少一个都编译不过。 下一阶段要落的四件事很具体:左对齐收、右对齐发的多行历史并带滚动条;用用户名或聊天 ID 做本地通讯录;把截图附件的上传可靠性补上;再给常用命令绑快捷键。外汇和贵金属杠杆高、滑点狠,这类面板只是辅助复盘,别拿它当信号源。 等下个版本把多行与附件补齐,MT5 里搓一个顺手的外部 API 交互就不远了。先开终端把这几个文件挂上,跑通第一阶段再说。

常见问题

把通信面板拆成独立可复用模块,用自定义对话框逻辑挂载截图与消息控件,编译后从面板直接调用即可。
在初始化阶段先检测对象是否存在再创建,缺失则补建并打日志,可避免重复创建导致的运行时崩溃。
小布可接管面板的消息与告警流,自动汇总异动并推送,你只需打开对应品种页看结论,不用手动刷面板。
编译通过后,拖入图表→首次容错建面板→填接收端→点发送,三步即可跑通消息流。
复用模块能省掉重复通信代码,但品种差异和容错仍要按需改,别把这条线当万能底层神坛供着。