在 MQL5 中创建交易管理面板(第九部分):代码组织(三):通信模块·进阶篇
对话框初始化与配色落点
在 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 面板存在滑点与事件阻塞风险,建议在模拟盘先验证。
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 可能清掉正在发的指令,属高风险操作环境。
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,能直观看到列表被整体下移。外汇与贵金属图表挂这类自定义面板属高风险操作,参数误改可能导致界面错位。
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 类里就能直接跑。
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); // 后续比对按钮名并转发
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);