在 MQL5 中创建交易管理面板(第九部分):代码组织(二):模块化·进阶篇
(2/3)· 还在单一 EA 里翻几百行找通信面板?拆成 mqh 模块后改一处不惊全身
管理面板对话框的骨架与尺寸常量
在 MT5 用标准库搭一个管理类弹窗,第一步是把控件容器的尺寸和边距先钉死。下面这段头文件片段定义了 CAdminHomeDialog 类,它继承自 CAppDialog,并在私有区放了四个 CButton 实例:交易管理、佣金、分析、显示全部。 面板主体宽高被宏写死为 335×350 像素,四向内边距统一 11,控件横向与纵向间隙各 5,单个按钮尺寸 250×40。这些数字直接决定你在图表上拖出来的面板会不会挤成一团,建议开 MT5 把 ADMIN_PANEL_WIDTH 改成 400 看看布局余量。 被注释掉的 #import "stdlib.ex5" 和 ErrorDescription 说明原本打算用标准库的错误描述函数,但当前节没接进来;若你后续要捕获按钮事件报错,得把这两行取消注释并确认 stdlib.ex5 已编译进终端。
class=class="str">"cmt">// class="macro">#class="kw">import "stdlib.ex5" class=class="str">"cmt">// class="type">class="kw">string ErrorDescription(class="type">int error_code); class=class="str">"cmt">// class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| AdminHomeDialog.mqh | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Controls\Dialog.mqh> class="macro">#include <Controls\Button.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Defines | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define ADMIN_PANEL_WIDTH(class="num">335) class="macro">#define ADMIN_PANEL_HEIGHT(class="num">350) class="macro">#define INDENT_LEFT(class="num">11) class="macro">#define INDENT_TOP(class="num">11) class="macro">#define INDENT_RIGHT(class="num">11) class="macro">#define INDENT_BOTTOM(class="num">11) class="macro">#define CONTROLS_GAP_X(class="num">5) class="macro">#define CONTROLS_GAP_Y(class="num">5) class="macro">#define BUTTON_WIDTH(class="num">250) class="macro">#define BUTTON_HEIGHT(class="num">40) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CAdminHomeDialog class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CAdminHomeDialog : class="kw">public CAppDialog { class="kw">private: CButton m_tradeMgmtButton; CButton m_commButton; CButton m_analyticsButton; CButton m_showAllButton; class="kw">public: CAdminHomeDialog(class="type">void) {} ~CAdminHomeDialog(class="type">void) {} class="kw">virtual class="type">bool Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2); class="kw">virtual class="type">bool OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam); };
「对话框创建与点击事件的硬接线」
在 MT5 自定义面板里,CAdminHomeDialog 的 Create() 必须先把父类 CAppDialog 的窗口画出来,再依次挂上四个功能按钮。只要其中任意一个 CreateTradeMgmtButton()、CreateCommButton()、CreateAnalyticsButton()、CreateShowAllButton() 返回 false,整个对话框就建不起来——这种链式失败检查能让你在初始化阶段就暴露资源缺失,而不是等点击没反应才排查。 OnEvent() 只拦截 CHARTEVENT_OBJECT_CLICK,用 sparam 比对各按钮的 Name() 来路由动作。注意每个分支都先 Print() 出被点对象名,这是调试 GUI 事件最直白的手法:实盘前在策略测试器里点一遍,看日志里是否依次打出 'Trade Management button detected' 等四行,就能确认事件绑定没断。 外汇与贵金属面板开发属高风险调试场景,按钮路由若写错可能触发误下单。建议把 Print 调试行保留到上线前最后一次编译,再批量注释掉。
class="type">bool CAdminHomeDialog::Create(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2) { if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) class="kw">return class="kw">false; if(!CreateTradeMgmtButton()) class="kw">return class="kw">false; if(!CreateCommButton()) class="kw">return class="kw">false; if(!CreateAnalyticsButton()) class="kw">return class="kw">false; if(!CreateShowAllButton()) class="kw">return class="kw">false; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Event Handling(Enhanced Debugging) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CAdminHomeDialog::OnEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { if(id == CHARTEVENT_OBJECT_CLICK) { Print("Clicked object: ", sparam); class=class="str">"cmt">// Debug which object was clicked if(sparam == m_tradeMgmtButton.Name()) { Print("Trade Management button detected"); OnClickTradeManagement(); class="kw">return true; } else if(sparam == m_commButton.Name()) { Print("Communications button detected"); OnClickCommunications(); class="kw">return true; } else if(sparam == m_analyticsButton.Name()) { Print("Analytics button detected"); OnClickAnalytics(); class="kw">return true; } else if(sparam == m_showAllButton.Name()) { Print("Show All button detected"); OnClickShowAll(); class="kw">return true; } } class="kw">return CAppDialog::OnEvent(id, lparam, dparam, sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 面板按钮的点击响应与坐标排布
在 MT5 自定义面板里,按钮点击先走轻量回调,只往终端日志打一行标识,方便你确认事件链路通没通。下面四个 handler 都挂在 CAdminHomeDialog 类下,点哪个面板就 Print 对应字符串,不碰业务逻辑,纯粹做路由探针。 CreateTradeMgmtButton 把第一个按钮钉在 (INDENT_LEFT, INDENT_TOP),宽高由 BUTTON_WIDTH / BUTTON_HEIGHT 决定;后续按钮的 y 坐标用 INDENT_TOP + (BUTTON_HEIGHT + CONTROLS_GAP_Y) * n 递推,n=1、2、3 分别对应通讯、分析、显示全部三个按钮,垂直间距完全由 CONTROLS_GAP_Y 控制。 注意 CreateShowAllButton 的 Text 里带了 "Show All 💥",这种非 ASCII 字符在 MQH 资源里能渲染,但跨语言系统可能乱码,建议正式版拿掉。CreateHiddenPanels 用 ExtDialog.Create(0,"Admin Home",0, MAIN_DIALOG_X, MAIN_DIALOG_Y, MAIN_DIALOG_X+MAIN_DIALOG_WIDTH, MAIN_DIALOG_Y+MAIN_DIALOG_HEIGHT) 在主图 0 号子窗口拉出总框,成功才继续挂子控件。 开 MT5 把这段塞进 EA 的 OnInit 前,改 BUTTON_HEIGHT=30、CONTROLS_GAP_Y=10 跑一下,日志里应看到四个 clicked 各触发一次,按钮垂直占位刚好 4*(30+10)=160 像素。外汇与贵金属图表加载这类面板属高风险操作环境,参数误写可能让对象不显示,先在模拟盘验证。
class=class="str">"cmt">//| Button Click Handlers | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CAdminHomeDialog::OnClickTradeManagement() { Print("Trade Management Panel clicked"); } class="type">void CAdminHomeDialog::OnClickCommunications() { Print("Communications Panel clicked"); } class="type">void CAdminHomeDialog::OnClickAnalytics() { Print("Analytics Panel clicked"); } class="type">void CAdminHomeDialog::OnClickShowAll() { Print("Show All clicked"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Control Creation Methods | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CAdminHomeDialog::CreateTradeMgmtButton() { class="type">int x = INDENT_LEFT; class="type">int y = INDENT_TOP; class="kw">return m_tradeMgmtButton.Create(m_chart_id, m_name+"_TradeBtn", m_subwin, x, y, x+BUTTON_WIDTH, y+BUTTON_HEIGHT) && m_tradeMgmtButton.Text("Trade Management Panel") && Add(m_tradeMgmtButton); } class="type">bool CAdminHomeDialog::CreateCommButton() { class="type">int x = INDENT_LEFT; class="type">int y = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y; class="kw">return m_commButton.Create(m_chart_id, m_name+"_CommBtn", m_subwin, x, y, x+BUTTON_WIDTH, y+BUTTON_HEIGHT) && m_commButton.Text("Communications Panel") && Add(m_commButton); } class="type">bool CAdminHomeDialog::CreateAnalyticsButton() { class="type">int x = INDENT_LEFT; class="type">int y = INDENT_TOP + (BUTTON_HEIGHT + CONTROLS_GAP_Y) * class="num">2; class="kw">return m_analyticsButton.Create(m_chart_id, m_name+"_AnalyticsBtn", m_subwin, x, y, x+BUTTON_WIDTH, y+BUTTON_HEIGHT) && m_analyticsButton.Text("Analytics Panel") && Add(m_analyticsButton); } class="type">bool CAdminHomeDialog::CreateShowAllButton() { class="type">int x = INDENT_LEFT; class="type">int y = INDENT_TOP + (BUTTON_HEIGHT + CONTROLS_GAP_Y) * class="num">3; class="kw">return m_showAllButton.Create(m_chart_id, m_name+"_ShowAllBtn", m_subwin, x, y, x+BUTTON_WIDTH, y+BUTTON_HEIGHT) && m_showAllButton.Text("Show All 💥") && Add(m_showAllButton); } class="macro">#include "AdminHomeDialog.mqh" CAdminHomeDialog ExtDialog; class="type">bool CreateHiddenPanels() { class="type">bool success = ExtDialog.Create(class="num">0, "Admin Home", class="num">0, MAIN_DIALOG_X, MAIN_DIALOG_Y, MAIN_DIALOG_X + MAIN_DIALOG_WIDTH, MAIN_DIALOG_Y + MAIN_DIALOG_HEIGHT); if(success)
把信号推到 Telegram 的底层写法
在 EA 面板鉴权通过后,图表事件会继续派发给对话框对象;未鉴权时面板直接隐藏,这段控制流决定了本地 UI 与远端通知能否并行工作。 真正把价格行为信号丢出去,靠的是一段独立的 include 文件。下面这个函数用 MT5 的 WebRequest 直连 Telegram Bot API,把 JSON 报文发到指定 chat_id。 超时写死 5000 毫秒,返回码 200 才认作发送成功,其余情况把 HTTP code 和 GetLastError 一起打印到日志,方便你查是网络层还是 token 问题。 引用方式有两种:放标准 Include 目录就写 #include <Telegram.mqh>;若塞在子文件夹,得写成 #include <FolderName\Telegram.mqh> 并把 FolderName 换成实际目录名,否则编译期就报找不到文件。外汇与贵金属行情波动剧烈,这类实时推送只作辅助提醒,任何信号都不构成入场保证,实盘前请在模拟盘验证推送延迟。
class="type">bool SendMessageToTelegram(class="type">class="kw">string message, class="type">class="kw">string chatId, class="type">class="kw">string botToken) { class="type">class="kw">string url = "https:class=class="str">"cmt">//api.telegram.org/bot" + botToken + "/sendMessage"; class="type">class="kw">string jsonMessage = "{\"chat_id\":\"" + chatId + "\", \"text\":\"" + message + "\"}"; class="type">char postData[]; ArrayResize(postData, StringToCharArray(jsonMessage, postData) - class="num">1); class="type">int timeout = class="num">5000; class="type">char result[]; class="type">class="kw">string responseHeaders; class="type">int responseCode = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, postData, result, responseHeaders); if (responseCode == class="num">200) { Print("Message sent successfully: ", message); class="kw">return true; } else { Print("Failed to send message. HTTP code: ", responseCode, " Error code: ", GetLastError()); Print("Response: ", CharArrayToString(result)); class="kw">return class="kw">false; } }
「把二次验证弹窗坐标写死在头文件里」
在 EA 的鉴权模块里,把对话框和输入框的像素坐标全部用 #define 固化进 authentication.mqh,是一种直白但脆弱的做法。上面这段头文件定义了主鉴权窗体 300×200、起点 (100,100),密码输入框宽 260、高 30,标签与反馈区也各有独立坐标。 二次验证弹窗 TWOFA 同样占 300×200,输入框左上角落在 (20,50)。登录与取消按钮 Y 轴同为 130,取消键 X=160,比登录键 X=20 右移了 140 像素,其中 20 像素是刻意留的间距。 这种硬编码方式让你在 MT5 里直接改数字就能调布局,但换分辨率或 DPI 缩放后可能错位。外汇与贵金属交易接入这类自动鉴权存在账户凭证泄露的高风险,实盘前务必在模拟环境验证。 别把坐标当万能 DPI 大于 100% 的 Windows 缩放会让这些绝对像素偏掉,真要上机先把显示设置调到 100% 再测一遍弹窗位置。
SendMessageToTelegram("Your verification code: " + ActiveTwoFactorAuthCode, TwoFactorAuthChatId, TwoFactorAuthBotToken) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| authentication.mqh | class=class="str">"cmt">//| Copyright class="num">2024, Clemence Benjamin | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, Clemence Benjamin" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.0" class="macro">#class="kw">property strict class=class="str">"cmt">// Authentication Dialog Coordinates class="macro">#define AUTH_DIALOG_X class="num">100 class="macro">#define AUTH_DIALOG_Y class="num">100 class="macro">#define AUTH_DIALOG_WIDTH class="num">300 class="macro">#define AUTH_DIALOG_HEIGHT class="num">200 class="macro">#define PASS_INPUT_X class="num">20 class="macro">#define PASS_INPUT_Y class="num">50 class="macro">#define PASS_INPUT_WIDTH class="num">260 class=class="str">"cmt">// Wider input field class="macro">#define PASS_INPUT_HEIGHT class="num">30 class="macro">#define PASS_LABEL_X class="num">20 class="macro">#define PASS_LABEL_Y class="num">20 class="macro">#define PASS_LABEL_WIDTH class="num">200 class="macro">#define PASS_LABEL_HEIGHT class="num">20 class="macro">#define FEEDBACK_LABEL_X class="num">20 class="macro">#define FEEDBACK_LABEL_Y class="num">100 class="macro">#define FEEDBACK_LABEL_WIDTH class="num">260 class="macro">#define FEEDBACK_LABEL_HEIGHT class="num">40 class=class="str">"cmt">// Button spacing adjustments class="macro">#define LOGIN_BTN_X class="num">20 class="macro">#define LOGIN_BTN_Y class="num">130 class="macro">#define LOGIN_BTN_WIDTH class="num">120 class="macro">#define LOGIN_BTN_HEIGHT class="num">30 class="macro">#define CANCEL_BTN_X class="num">160 class=class="str">"cmt">// Added 20px spacing from login button class="macro">#define CANCEL_BTN_Y class="num">130 class="macro">#define CANCEL_BTN_WIDTH class="num">120 class="macro">#define CANCEL_BTN_HEIGHT class="num">30 class=class="str">"cmt">// Two-Factor Authentication Dialog Coordinates class="macro">#define TWOFA_DIALOG_X class="num">100 class="macro">#define TWOFA_DIALOG_Y class="num">100 class="macro">#define TWOFA_DIALOG_WIDTH class="num">300 class="macro">#define TWOFA_DIALOG_HEIGHT class="num">200 class="macro">#define TWOFA_INPUT_X class="num">20 class="macro">#define TWOFA_INPUT_Y class="num">50
◍ 双因子弹窗的布局与类骨架
给 EA 加一层 Telegram 双因子校验,第一步是把对话框的像素坐标定死。下面这组宏把密码框、2FA 输入框、提示标签和按钮的 X/Y/宽高全部写成常量:输入框 180×30,标签区 260×20,反馈文本区放到 Y=100 处占 40 像素高,验证与取消按钮同在 Y=130 一行,宽度分别 120 和 60。 类 CAuthenticationManager 用 CDialog 分别挂了主鉴权窗和 2FA 窗,私有成员里 m_failedAttempts 初始为 0、m_isAuthenticated 初始 false、m_active2faCode 初始空串,构造函数直接吃进 password、chatId、botToken 三个参数。 Initialize() 里先建两个窗体,随后立刻 m_2faDialog.Hide(),保证 2FA 弹窗在密码未过之前不露面。开 MT5 把这段宏和类头抄进新 EA,编译后能在初始化日志看到窗体是否创建成功,若 Print 出 Authentication initialization failed 就说明控件库路径不对。 外汇与贵金属品种波动剧烈,这类本地鉴权只防误操作不防平台风险,实盘前先在策略测试器跑通事件流。
class="macro">#define TWOFA_INPUT_WIDTH class="num">180 class="macro">#define TWOFA_INPUT_HEIGHT class="num">30 class="macro">#define TWOFA_LABEL_X class="num">20 class="macro">#define TWOFA_LABEL_Y class="num">20 class="macro">#define TWOFA_LABEL_WIDTH class="num">260 class="macro">#define TWOFA_LABEL_HEIGHT class="num">20 class="macro">#define TWOFA_FEEDBACK_X class="num">20 class="macro">#define TWOFA_FEEDBACK_Y class="num">100 class="macro">#define TWOFA_FEEDBACK_WIDTH class="num">260 class="macro">#define TWOFA_FEEDBACK_HEIGHT class="num">40 class="macro">#define TWOFA_VERIFY_BTN_X class="num">60 class="macro">#define TWOFA_VERIFY_BTN_Y class="num">130 class="macro">#define TWOFA_VERIFY_WIDTH class="num">120 class="macro">#define TWOFA_VERIFY_HEIGHT class="num">30 class="macro">#define TWOFA_CANCEL_BTN_X class="num">140 class="macro">#define TWOFA_CANCEL_BTN_Y class="num">130 class="macro">#define TWOFA_CANCEL_WIDTH class="num">60 class="macro">#define TWOFA_CANCEL_HEIGHT class="num">30 class="macro">#include <Controls\Dialog.mqh> class="macro">#include <Controls\Button.mqh> class="macro">#include <Controls\Edit.mqh> class="macro">#include <Controls\Label.mqh> class="macro">#include <Telegram.mqh> class CAuthenticationManager { class="kw">private: CDialog m_authDialog; CDialog m_2faDialog; CEdit m_passwordInput; CEdit m_2faCodeInput; CLabel m_passwordLabel; CLabel m_feedbackLabel; CLabel m_2faLabel; CLabel m_2faFeedback; CButton m_loginButton; CButton m_closeAuthButton; CButton m_2faLoginButton; CButton m_close2faButton; class="type">class="kw">string m_password; class="type">class="kw">string m_2faChatId; class="type">class="kw">string m_2faBotToken; class="type">int m_failedAttempts; class="type">bool m_isAuthenticated; class="type">class="kw">string m_active2faCode; class="kw">public: CAuthenticationManager(class="type">class="kw">string password, class="type">class="kw">string twoFactorChatId, class="type">class="kw">string twoFactorBotToken) : m_password(password), m_2faChatId(twoFactorChatId), m_2faBotToken(twoFactorBotToken), m_failedAttempts(class="num">0), m_isAuthenticated(class="kw">false), m_active2faCode("") { } ~CAuthenticationManager() { m_authDialog.Destroy(); m_2faDialog.Destroy(); } class="type">bool Initialize() { if(!CreateAuthDialog() || !Create2FADialog()) { Print("Authentication initialization failed"); class="kw">return class="kw">false; } m_2faDialog.Hide(); class=class="str">"cmt">// Ensure 2FA dialog starts hidden class="kw">return true; } class="type">bool IsAuthenticated() class="kw">const { class="kw">return m_isAuthenticated; } class="type">void HandleEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) {