在 MQL5 中创建交易管理面板(第九部分):代码组织(三):通信模块·进阶篇
📘

在 MQL5 中创建交易管理面板(第九部分):代码组织(三):通信模块·进阶篇

第 2/3 篇

对话框初始化与配色落点

在 MT5 自定义面板开发里,CCommunicationDialog 的 Create 方法是整个通讯面板的入口。它先调用基类 CAppDialog::Create 铺好坐标与子窗口,失败直接返回 false,这步决定了面板能否挂到指定图表上。 随后用 ObjectSetInteger 给 Back、Client、Caption 等对象刷背景与文字色。注意 m_panel_flag 为假时才去设 Border 的 BGCOLOR 与 BORDER_COLOR——很多人漏了这个条件判断,导致无边框模式下硬写边框属性却不显示。 控件按序创建:输入框、清除键、发送键、快捷标签、快捷消息列表,任何一个返回 false 就中断。最后 m_inputBox.Text(m_quickMessages[0]) 把数组第 0 项塞进输入框并 ChartRedraw,意味着面板一打开就有预设文本,省去手动输入。 跑这段代码前,确认 CLR_PANEL_BG、CLR_CLIENT_BG 等宏已在头文件定义,否则编译期就报未声明。外汇与贵金属图表加载此类 EA 面板存在滑点与事件阻塞风险,建议在模拟盘先验证。

MQL5 / C++
class=class="str">"cmt">//| Destructor                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CCommunicationDialog::~CCommunicationDialog()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create Method                                                              |
class=class="str">"cmt">//| Initializes the dialog and its controls with full class="type">color styling      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCommunicationDialog::Create(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)
{
  class=class="str">"cmt">// Create the base dialog
  if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2))
      class="kw">return(false);

  Caption("Communications Panel"); class=class="str">"cmt">// Set the title

  class=class="str">"cmt">// Set dialog background class="type">color
  ObjectSetInteger(m_chart_id, m_name + "Back", OBJPROP_BGCOLOR, CLR_PANEL_BG);

  class=class="str">"cmt">// Set client area background class="type">color
  ObjectSetInteger(m_chart_id, m_name + "Client", OBJPROP_BGCOLOR, CLR_CLIENT_BG);

  class=class="str">"cmt">// Set caption colors
  ObjectSetInteger(m_chart_id, m_name + "Caption", OBJPROP_BGCOLOR, CLR_CAPTION_BG);
  ObjectSetInteger(m_chart_id, m_name + "Caption", OBJPROP_COLOR, CLR_CAPTION_TEXT);

  class=class="str">"cmt">// Set border colors(if border exists, i.e., m_panel_flag is false)
  if(!m_panel_flag)
  {
      ObjectSetInteger(m_chart_id, m_name + "Border", OBJPROP_BGCOLOR, CLR_BORDER_BG);
      ObjectSetInteger(m_chart_id, m_name + "Border", OBJPROP_BORDER_COLOR, CLR_BORDER);
  }

  class=class="str">"cmt">// Create all controls
  if(!CreateInputBox())
      class="kw">return(false);
  if(!CreateClearButton())
      class="kw">return(false);
  if(!CreateSendButton())
      class="kw">return(false);
  if(!CreateQuickMsgLabel())
      class="kw">return(false);
  if(!CreateQuickMessageList())
      class="kw">return(false);

  class=class="str">"cmt">// Set initial text in input box
  m_inputBox.Text(m_quickMessages[class="num">0]);
  ChartRedraw();
  class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CreateInputBox                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCommunicationDialog::CreateInputBox(class="type">void)
{
  class="type">int x1 = COMMS_MARGIN_LEFT;
  class="type">int y1 = COMMS_MARGIN_TOP;

「对话框输入框与按钮的布局落点」

在 MT5 自定义面板里,输入框右侧坐标用 ClientAreaWidth() 减去右边距 COMMS_MARGIN_RIGHT 来算,底部则落在 y1 + 3 * COMMS_INPUT_HEIGHT。这样输入框高度固定占 3 行输入区,和上方坐标形成硬绑定,改 COMMS_INPUT_HEIGHT 会整体推高下方按钮。 下面这段是输入框创建与配色,失败就打印并返回 false,成功才挂进容器并设背景、文字色。 Clear 与 Send 按钮的纵向起点一致:COMMS_MARGIN_TOP + 3 * COMMS_INPUT_HEIGHT + COMMS_GAP_VERTICAL,也就是紧贴输入框下方留一条竖向间隙。Send 按钮的 x1 取 Clear 按钮右沿再加 COMMS_GAP_VERTICAL,两个按钮等宽 COMMS_BUTTON_WIDTH 并排,间距由竖向间隙常量复用,少一个宏就少一处错位风险。 跑起来若按钮重叠,优先查 COMMS_GAP_VERTICAL 是否被误设为 0——很多面板拥挤 bug 出自这里。外汇与贵金属图表加载这类 EA 控件时波动快,误触 Clear 可能清掉正在发的指令,属高风险操作环境。

MQL5 / C++
  class="type">int x2 = ClientAreaWidth() - COMMS_MARGIN_RIGHT;
  class="type">int y2 = y1 + class="num">3 * COMMS_INPUT_HEIGHT;
  if(!m_inputBox.Create(m_chart_id, m_name + "_InputBox", m_subwin, x1, y1, x2, y2))
  {
      Print("Failed to create InputBox");
      class="kw">return(false);
  }
  if(!Add(m_inputBox))
      class="kw">return(false);

  ObjectSetInteger(m_chart_id, m_name + "_InputBox", OBJPROP_BGCOLOR, CLR_INPUT_BG);
  ObjectSetInteger(m_chart_id, m_name + "_InputBox", OBJPROP_COLOR, CLR_INPUT_TEXT);
  class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CreateClearButton                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCommunicationDialog::CreateClearButton(class="type">void)
{
   class="type">int button_y1 = COMMS_MARGIN_TOP + class="num">3 * COMMS_INPUT_HEIGHT + COMMS_GAP_VERTICAL;
   class="type">int x1 = COMMS_MARGIN_LEFT;
   class="type">int x2 = x1 + COMMS_BUTTON_WIDTH;
   class="type">int y2 = button_y1 + COMMS_BUTTON_HEIGHT;
   if(!m_clearButton.Create(m_chart_id, m_name + "_ClearButton", m_subwin, x1, button_y1, x2, y2))
   {
      Print("Failed to create ClearButton");
      class="kw">return(false);
   }
   m_clearButton.Text("Clear");
   if(!Add(m_clearButton))
      class="kw">return(false);

   ObjectSetInteger(m_chart_id, m_name + "_ClearButton", OBJPROP_BGCOLOR, CLR_CLEAR_BG);
   ObjectSetInteger(m_chart_id, m_name + "_ClearButton", OBJPROP_COLOR, CLR_BUTTON_TEXT);
   class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CreateSendButton                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CCommunicationDialog::CreateSendButton(class="type">void)
{
   class="type">int button_y1 = COMMS_MARGIN_TOP + class="num">3 * COMMS_INPUT_HEIGHT + COMMS_GAP_VERTICAL;
   class="type">int clear_button_x2 = COMMS_MARGIN_LEFT + COMMS_BUTTON_WIDTH;
   class="type">int x1 = clear_button_x2 + COMMS_GAP_VERTICAL;
   class="type">int x2 = x1 + COMMS_BUTTON_WIDTH;
   class="type">int y2 = button_y1 + COMMS_BUTTON_HEIGHT;
   if(!m_sendButton.Create(m_chart_id, m_name + "_SendButton", m_subwin, x1, button_y1, x2, y2))
   {
      Print("Failed to create SendButton");
      class="kw">return(false);
   }
   m_sendButton.Text("Send");
   if(!Add(m_sendButton))
      class="kw">return(false);

   ObjectSetInteger(m_chart_id, m_name + "_SendButton", OBJPROP_BGCOLOR, CLR_SEND_BG);
   ObjectSetInteger(m_chart_id, m_name + "_SendButton", OBJPROP_COLOR, CLR_BUTTON_TEXT);
   class="kw">return(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 对话框里快捷消息区的坐标与显隐逻辑

在自定义通讯面板里,快捷消息标签和列表的摆放完全靠一组宏常量算出来。标签顶边 label_y1 从 COMMS_MARGIN_TOP 起,叠了 3 个输入行高度、按钮高度和两段垂直间隙,列表顶边再往下多压一个标签高和一段间隙,这种累加法能保证控件不重叠。 CreateQuickMsgLabel 里先按 (x1,label_y1,x2,y2) 建 Label,失败就 Print 并返回 false;成功设文本 "QuickMessages" 后挂进容器,再用 ObjectSetInteger 改 OBJPROP_COLOR 为 CLR_LABEL_TEXT。CreateQuickMessageList 同理建 ListView,背景与文字色分别走 CLR_LIST_BG、CLR_LIST_TEXT,随后把 m_quickMessages 数组逐项 AddItem 成 "Message: xxx"。 Toggle 不返回值,靠 IsVisible 判断当前状态:可见就 Hide 否则 Show,末尾必调 ChartRedraw 让副图立刻重绘。开 MT5 把这几段塞进你的 CCommunicationDialog 类,改 COMMS_GAP_VERTICAL 从默认 5 到 8,能直观看到列表被整体下移。外汇与贵金属图表挂这类自定义面板属高风险操作,参数误改可能导致界面错位。

MQL5 / C++
class="type">bool CCommunicationDialog::CreateQuickMsgLabel(class="type">void)
{
   class="type">int label_y1 = COMMS_MARGIN_TOP + class="num">3 * COMMS_INPUT_HEIGHT + COMMS_GAP_VERTICAL +
                  COMMS_BUTTON_HEIGHT + COMMS_GAP_VERTICAL;
   class="type">int x1 = COMMS_MARGIN_LEFT;
   class="type">int x2 = x1 + COMMS_LISTVIEW_WIDTH;
   class="type">int y2 = label_y1 + COMMS_LABEL_HEIGHT;
   if(!m_quickMsgLabel.Create(m_chart_id, m_name + "_QuickMsgLabel", m_subwin, x1, label_y1, x2, y2))
   {
      Print("Failed to create QuickMessages Label");
      class="kw">return(false);
   }
   m_quickMsgLabel.Text("QuickMessages");
   if(!Add(m_quickMsgLabel))
      class="kw">return(false);

   ObjectSetInteger(m_chart_id, m_name + "_QuickMsgLabel", OBJPROP_COLOR, CLR_LABEL_TEXT);
   class="kw">return(true);
}

class="type">bool CCommunicationDialog::CreateQuickMessageList(class="type">void)
{
   class="type">int list_y1 = COMMS_MARGIN_TOP + class="num">3 * COMMS_INPUT_HEIGHT + COMMS_GAP_VERTICAL +
                 COMMS_BUTTON_HEIGHT + COMMS_GAP_VERTICAL +
                 COMMS_LABEL_HEIGHT + COMMS_GAP_VERTICAL;
   class="type">int x1 = COMMS_MARGIN_LEFT;
   class="type">int x2 = x1 + COMMS_LISTVIEW_WIDTH;
   class="type">int y2 = list_y1 + COMMS_LISTVIEW_HEIGHT;
   if(!m_quickMessageList.Create(m_chart_id, m_name + "_QuickMsgList", m_subwin, x1, list_y1, x2, y2))
   {
      Print("Failed to create ListView");
      class="kw">return(false);
   }
   if(!Add(m_quickMessageList))
      class="kw">return(false);

   ObjectSetInteger(m_chart_id, m_name + "_QuickMsgList", OBJPROP_BGCOLOR, CLR_LIST_BG);
   ObjectSetInteger(m_chart_id, m_name + "_QuickMsgList", OBJPROP_COLOR, CLR_LIST_TEXT);

   for(class="type">int i = class="num">0; i < ArraySize(m_quickMessages); i++)
   {
      if(!m_quickMessageList.AddItem("Message: " + m_quickMessages[i]))
         class="kw">return(false);
   }

   class="kw">return(true);
}

class="type">void CCommunicationDialog::Toggle()
{
   if(IsVisible())
      Hide();
   else
      Show();
   ChartRedraw();
}

对话框事件怎么路由到发信动作

在 MT5 自定义面板里,图表对象点击事件统一走 OnEvent 虚函数。上面的实现把 CHARTEVENT_OBJECT_CLICK 里的 sparam 和按钮 Name() 做比对:命中发送键就调 OnClickSend,命中清除键就调 OnClickClear,两者都直接 return true 截断事件,避免父类再处理一次。 下拉快捷消息走的是另一条路:当 id 为 ON_CHANGE 且 sparam 等于列表名时,用 lparam 里的选中下标去 m_quickMessages 数组取文本,再写回 m_inputBox。这里用 ArraySize 做了边界判断,selectedIndex 在 0 到数组长度减一之间才赋值,防止越界崩面板。 发信函数本身不复杂:OnClickSend 先读输入框文本,空串就 Print 提示不发送;非空则调 SendMessageToTelegram(message, m_chatId, m_botToken),成功或失败都只打印日志,不弹窗。注意外汇与贵金属交易接口调用涉及网络与账户权限,属于高风险操作,实盘前务必在模拟账户验证 token 与 chatId 配置。 OnClickClear 更轻量,把输入框设空串后调 ChartRedraw 强制重绘,再打印清除日志。整套事件分发没有用定时器轮询,完全靠 GUI 事件驱动,CPU 占用倾向更低,开 MT5 把这段代码塞进你的 panel 类里就能直接跑。

MQL5 / C++
class="type">bool CCommunicationDialog::OnEvent(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)
   {
      if(sparam == m_sendButton.Name())
      {
         OnClickSend();
         class="kw">return true;
      }
      else if(sparam == m_clearButton.Name())
      {
         OnClickClear();
         class="kw">return true;
      }
   }
   else if(id == ON_CHANGE && sparam == m_quickMessageList.Name())
   {
      class="type">int selectedIndex = (class="type">int)lparam;
      if(selectedIndex >= class="num">0 && selectedIndex < ArraySize(m_quickMessages))
      {
         m_inputBox.Text(m_quickMessages[selectedIndex]);
      }
      class="kw">return true;
   }
   class="kw">return CAppDialog::OnEvent(id, lparam, dparam, sparam);
}
class="type">void CCommunicationDialog::OnClickSend()
{
   class="type">class="kw">string message = m_inputBox.Text();
   if(message != "")
   {
      if(SendMessageToTelegram(message, m_chatId, m_botToken))
         Print("Message sent to Telegram: ", message);
      else
         Print("Failed to send message to Telegram");
   }
   else
   {
      Print("No message to send - input box is empty");
   }
}
class="type">void CCommunicationDialog::OnClickClear()
{
   m_inputBox.Text("");  class=class="str">"cmt">// Clear the input box
   ChartRedraw();
   Print("Input box cleared.");
}

「把通信面板挂进主对话框的事件链」

把 CommunicationsDialog 接进 AdminHomeDialog,核心不是复制代码,而是让两个对话框共享一套事件循环。先在 AdminHomeDialog.mqh 顶部、其他 include 之后补一行 #include <CommunicationsDialog.mqh>,MQL5 才会把 CCommunicationDialog 类编进来;漏掉这行,编译器根本不认通信面板。 类内部用 CCommunicationDialog *m_commPanel 留一个指针位,配合 m_chatId、m_botToken 两个私有字符串存 Telegram 参数。构造函数通过初始化列表把 chatId、botToken 收进来,同时把 m_commPanel 置 NULL——面板不立刻建,等用户点按钮再延迟实例化,省的是图表资源。 析构里判 if(m_commPanel) 再 delete 并归 NULL,避免重复释放导致 MT5 终端崩溃。点击“通信”按钮走 OnClickCommunications:面板为空才 new,传入 chatId/botToken;Create 坐标写死为左上角 (20,435)、宽 300 高 350,建失败就打印错误并回退 NULL。成功或已存在都调 Toggle() 做显示切换。 事件穿透才是集成关键。OnEvent 收到 CHARTEVENT_OBJECT_CLICK 先比对是不是 m_commButton 名的点击;否则进 else if(m_commPanel != NULL && m_commPanel.IsVisible()),把 id、lparam 等直接甩给 m_commPanel.OnEvent,让通信面板自己处理“发送/清除”等子控件。返回它给的布尔值,两个对话框就能各管各的点击又不打架。 下面这段是集成骨架的浓缩版,逐行对应上面五步: #include <CommunicationsDialog.mqh> // 引入通信面板头文件,打开类可见性 class CAdminHomeDialog : public CAppDialog { private: CCommunicationDialog *m_commPanel; // 面板指针,初始不分配 string m_chatId; // Telegram 会话 ID string m_botToken; // Telegram 机器人令牌 }; CAdminHomeDialog::CAdminHomeDialog(string chatId, string botToken) : m_commPanel(NULL), m_chatId(chatId), m_botToken(botToken) {} // 构造时仅存参,面板留空 CAdminHomeDialog::~CAdminHomeDialog(void) { if(m_commPanel) { delete m_commPanel; m_commPanel = NULL; } // 析构释放,防悬垂指针 } void CAdminHomeDialog::OnClickCommunications() { if(m_commPanel == NULL) { m_commPanel = new CCommunicationDialog(m_chatId, m_botToken); // 延迟建面板传参 if(m_commPanel == NULL) { Print("Error: Failed to allocate"); return; } if(!m_commPanel.Create(m_chart_id, "CommPanel", m_subwin, 20, 435, 320, 785)) { Print("Error: Failed to create"); delete m_commPanel; m_commPanel = NULL; return; } } m_commPanel.Toggle(); // 已建则翻显隐状态 } bool CAdminHomeDialog::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_OBJECT_CLICK) { Print("Clicked object: ", sparam); // 后续比对按钮名并转发

MQL5 / C++
class="macro">#include <CommunicationsDialog.mqh>  class=class="str">"cmt">// 使用增强的通信对话框
class CAdminHomeDialog : class="kw">public CAppDialog
{
class="kw">private:
  CCommunicationDialog *m_commPanel; class=class="str">"cmt">// Pointer to the Communications panel
  class="type">class="kw">string                m_chatId;    class=class="str">"cmt">// Telegram Chat ID
  class="type">class="kw">string                m_botToken;  class=class="str">"cmt">// Telegram Bot Token

class=class="str">"cmt">///.................Space for other members e.g. buttons
};
CAdminHomeDialog::CAdminHomeDialog(class="type">class="kw">string chatId, class="type">class="kw">string botToken)
  : m_commPanel(NULL), m_chatId(chatId), m_botToken(botToken)
{
}
CAdminHomeDialog::~CAdminHomeDialog(class="type">void)
{
  if(m_commPanel)
  {
    class="kw">delete m_commPanel;
    m_commPanel = NULL;
  }
}
class="type">void CAdminHomeDialog::OnClickCommunications()
{
  if(m_commPanel == NULL)
  {
    m_commPanel = new CCommunicationDialog(m_chatId, m_botToken); class=class="str">"cmt">// Pass chatId and botToken
    if(m_commPanel == NULL)
    {
      Print("Error: Failed to allocate Communications panel");
      class="kw">return;
    }
    if(!m_commPanel.Create(m_chart_id, "CommPanel", m_subwin, class="num">20, class="num">435, class="num">20 + class="num">300, class="num">435 + class="num">350))
    {
      Print("Error: Failed to create Communications panel");
      class="kw">delete m_commPanel;
      m_commPanel = NULL;
      class="kw">return;
    }
  }
  m_commPanel.Toggle();
}
class="type">bool CAdminHomeDialog::OnEvent(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)
  {
    Print("Clicked object: ", sparam);

常见问题

用相对坐标算落点,先定对话框客户区边距,再按行高和控件宽递推 x/y,别写死绝对像素,换分辨率就乱。
给快捷消息区单独设可见标志位,仅在展开状态下重绘并让出输入框纵向空间,收起时直接隐藏不占布局。
可以,小布能按你贴的事件链代码指出哪一步没把对话框事件接到发信函数,并提示补回调的位置。
先确认主对话框 OnEvent 里是否分发到通信模块处理函数,再看发信动作是否绑了正确控件 ID 和消息体。
把配色抽成常量在初始化时统一赋值给对话框背景与控件画笔,别在各控件里硬编码颜色值。