在MQL5中创建交易管理员面板(第四部分):登录安全层·进阶篇
(2/3)· 面板裸奔等于把广播社区的话筒交给陌生人,本篇用密码层堵住这个口子
Telegram 喊单面板的初始化与鉴权骨架
想在 MT5 里做一个能直接发 Telegram 消息的盯盘面板,第一步是把版本号和依赖库钉死。上面代码里 #property version "1.19" 标了脚本版本,而 #include <Trade\Trade.mqh> 与 <Controls\*.mqh> 一组标准界面库,是后面画按钮、输入框的底子。
输入参数段给了 8 个快捷话术(QuickMessage1~8),从 "Updates" 到 "Invalid Signal",外加 Telegram 的 ChatId 和 BotToken 两个空串占位——实盘前你必须手动填,否则发不出去。全局里 MAX_MESSAGE_LENGTH = 4096 对应 Telegram API 单条消息上限,超了会被截断。
OnInit 里的逻辑很硬:先 ShowAuthenticationPrompt() 弹密码框,默认密码写死成 "2024"(代码里明标了 Default password),鉴权不过直接 INIT_FAILED 退出。过了才建 500×500 的 Admin Panel 并立刻 Hide(),也就是说面板默认藏起,等你触发再显。
别把默认密码当回事
代码里 string Password = "2024" 只是演示用,挂实盘前务必改成随机串,否则别人开你终端就能发指令。外汇和贵金属波动剧烈,这类面板只管发信号,下单风险仍由你扛。
class="macro">#class="kw">property version "class="num">1.19" class="macro">#include <Trade\Trade.mqh> 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=class="str">"cmt">// Input parameters input class="type">class="kw">string QuickMessage1 = "Updates"; input class="type">class="kw">string QuickMessage2 = "Close all"; input class="type">class="kw">string QuickMessage3 = "In deep profits"; input class="type">class="kw">string QuickMessage4 = "Hold position"; input class="type">class="kw">string QuickMessage5 = "Swing Entry"; input class="type">class="kw">string QuickMessage6 = "Scalp Entry"; input class="type">class="kw">string QuickMessage7 = "Book profit"; input class="type">class="kw">string QuickMessage8 = "Invalid Signal"; input class="type">class="kw">string InputChatId = "Enter Chat ID from Telegram bot API"; input class="type">class="kw">string InputBotToken = "Enter BOT TOKEN from your Telegram bot"; class=class="str">"cmt">// Global variables CDialog adminPanel; CDialog authentication; class=class="str">"cmt">// Renamed from passwordPanel CButton sendButton, clearButton, changeFontButton, toggleThemeButton, loginButton, closeAuthButton; CButton quickMessageButtons[class="num">8], minimizeButton, maximizeButton, closeButton; CEdit inputBox, passwordInputBox; CLabel charCounter, passwordPromptLabel; class="type">bool minimized = false; class="type">bool darkTheme = false; class="type">int MAX_MESSAGE_LENGTH = class="num">4096; class="type">class="kw">string availableFonts[] = { "Arial", "Courier New", "Verdana", "Times New Roman", "Britannic Bold", "Dubai Medium", "Impact", "Ink Tree", "Brush Script MT"}; class="type">int currentFontIndex = class="num">0; class=class="str">"cmt">// Default password for authentication class="type">class="kw">string Password = "class="num">2024"; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if (!ShowAuthenticationPrompt()) { Print("Authorization failed. Exiting..."); class="kw">return INIT_FAILED; class=class="str">"cmt">// Exit if the authorization fails } class=class="str">"cmt">// Initialize the main admin panel if (!adminPanel.Create(ChartID(), "Admin Panel", class="num">0, class="num">30, class="num">30, class="num">500, class="num">500)) { Print("Failed to create admin panel dialog"); class="kw">return INIT_FAILED; } class=class="str">"cmt">// Create controls for the admin panel if (!CreateControls()) { Print("Control creation failed"); class="kw">return INIT_FAILED; } class=class="str">"cmt">// Initially hide the admin panel adminPanel.Hide(); Print("Initialization complete"); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+
「在MT5图表上拼一个登录弹窗」
给EA或指标加权限层时,最轻量的做法不是另写外部程序,而是直接在主图用标准图形库画出认证对话框。下面这段函数把密码框、提示语、登录与关闭按钮一次性挂到当前图表,坐标以像素为单位,窗体左上角固定在(100,100)、宽500高300。 ShowAuthenticationPrompt() 先建总容器 authentication,任何子控件都要 Add 进去才会随窗体显示;若 Create 返回 false,立即 Print 报错并退出,避免空指针后续崩溃。 密码框 passwordInputBox 放在(20,70),尺寸260x95;提示标签在(20,20)写「Enter password: Access Admin Panel」;登录按钮(20,120)宽100,关闭按钮(120,120)宽200,两者Y对齐。最后 authentication.Show() 加 ChartRedraw() 才会立刻重绘,否则可能要等下一次事件才露面。 外汇与贵金属品种波动剧烈、杠杆风险高,这类本地弹窗只做界面演示,密码明文写在客户端代码里并不安全,实盘别直接照搬。
class="type">bool ShowAuthenticationPrompt() { if (!authentication.Create(ChartID(), "Authentication", class="num">0, class="num">100, class="num">100, class="num">500, class="num">300)) { Print("Failed to create authentication dialog"); class="kw">return false; } class=class="str">"cmt">// Create password input if (!passwordInputBox.Create(ChartID(), "PasswordInputBox", class="num">0, class="num">20, class="num">70, class="num">260, class="num">95)) { Print("Failed to create password input box"); class="kw">return false; } authentication.Add(passwordInputBox); class=class="str">"cmt">// Create prompt label if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", class="num">0, class="num">20, class="num">20, class="num">260, class="num">20)) { Print("Failed to create password prompt label"); class="kw">return false; } passwordPromptLabel.Text("Enter password: Access Admin Panel"); authentication.Add(passwordPromptLabel); class=class="str">"cmt">// Create login button if (!loginButton.Create(ChartID(), "LoginButton", class="num">0, class="num">20, class="num">120, class="num">100, class="num">140)) { Print("Failed to create login button"); class="kw">return false; } loginButton.Text("Login"); authentication.Add(loginButton); class=class="str">"cmt">// Create close button for authentication if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", class="num">0, class="num">120, class="num">120, class="num">200, class="num">140)) class=class="str">"cmt">// Adjusted position { Print("Failed to create close button for authentication"); class="kw">return false; } closeAuthButton.Text("Close"); authentication.Add(closeAuthButton); authentication.Show(); class=class="str">"cmt">// Show the authentication dialog ChartRedraw(); class=class="str">"cmt">// Redraw the chart to reflect changes class="kw">return true; class=class="str">"cmt">// Prompt shown successfully }
◍ 图表事件怎么分流到各个按钮
在 MT5 的面板类 EA 里,所有鼠标点击、编辑框失焦都先汇总到 OnChartEvent,靠事件 id 和 sparam 字符串判断用户点了哪个物件。上面这段把登录弹窗和主界面按钮全塞进同一个函数,逻辑集中但容易漏配 ID。 先看前半段:用 if(id==CHARTEVENT_OBJECT_CLICK) 拦下所有点击事件,再比 sparam 是不是 "LoginButton" 或 "CloseAuthButton"。注释里特意写了 Made sure this matches the ID,说明按钮的 OBJPROP_NAME 必须和字符串一字不差,否则永远进不了分支。 后面又接了一个 switch(id),在 CHARTEVENT_OBJECT_CLICK 下继续用 sparam 分流 SendButton、ClearButton、ToggleThemeButton 等 9 个主界面控件;其中 QuickMessageButton 用 StringFind 模糊匹配,再 StringSubstr(sparam,18) 截出序号转整型,减 1 后传给处理函数。 CHARTEVENT_OBJECT_ENDEDIT 只盯 "InputBox",失焦就触发 OnInputChange。实盘中外汇和贵金属波动剧烈、滑点频繁,这类面板若卡在错误分支,可能漏发信号或重复下单,建议把各按钮 ID 打印到 Experts 日志自查一次。
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) { if (id == CHARTEVENT_OBJECT_CLICK) { class=class="str">"cmt">// Handle button clicks inside the authentication dialog if (sparam == "LoginButton") { OnLoginButtonClick(); class=class="str">"cmt">// Call the login button handler } else if (sparam == "CloseAuthButton") class=class="str">"cmt">// Made sure this matches the ID { OnCloseAuthButtonClick(); class=class="str">"cmt">// Call the close button handler } } class="kw">switch (id) { case CHARTEVENT_OBJECT_CLICK: if (sparam == "SendButton") OnSendButtonClick(); else if (sparam == "ClearButton") OnClearButtonClick(); else if (sparam == "ChangeFontButton") OnChangeFontButtonClick(); else if (sparam == "ToggleThemeButton") OnToggleThemeButtonClick(); else if (sparam == "MinimizeButton") OnMinimizeButtonClick(); else if (sparam == "MaximizeButton") OnMaximizeButtonClick(); else if (sparam == "CloseButton") OnCloseButtonClick(); else if (StringFind(sparam, "QuickMessageButton") != -class="num">1) { class="type">long index = StringToInteger(StringSubstr(sparam, class="num">18)); OnQuickMessageButtonClick(index - class="num">1); } class="kw">break; case CHARTEVENT_OBJECT_ENDEDIT: if (sparam == "InputBox") OnInputChange(); class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle login button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnLoginButtonClick() { class="type">class="kw">string enteredPassword = passwordInputBox.Text(); if (enteredPassword == Password) class=class="str">"cmt">// Check the entered password {
EA 面板的鉴权销毁与控件拼装
这段逻辑解决一个很实际的问题:用户在 MT5 自定义面板里输完密码,对了就进后台,错了就清框重来;要是直接把鉴权窗关了,EA 自己也得撤,别留个空壳在图表上。 authentication.Destroy() 负责销毁鉴权对话框,成功时 Print 打日志并调 adminPanel.Show() 把管理面板亮出来;密码不对则提示重输,并用 passwordInputBox.Text("") 把输入框清空。若用户点关闭按钮触发 OnCloseAuthButtonClick,除了销毁窗体还会调 ExpertRemove() 卸载 EA,避免无鉴权状态挂机。 CreateControls() 里用 ChartID() 拿图表句柄,依次建输入框、字符计数器、清除与发送按钮,坐标都是像素级写死:输入框占 (5,25)-(460,95),计数器贴在 (380,5)-(460,25),清除键 (235,95)-(345,125),发送键 (350,95)-(460,125)。任何一步 Create 失败就返回 false,面板拼装中途断掉能立刻在日志里看到哪块没建出来。 开 MT5 把这几段塞进你的面板类里,重点看 OnCloseAuthButtonClick 有没有接 ExpertRemove——漏了这行,关窗后 EA 仍可能偷偷跑。
authentication.Destroy(); class=class="str">"cmt">// Hide the authentication dialog Print("Authentication successful."); adminPanel.Show(); class=class="str">"cmt">// Show the admin panel after successful authentication } else { Print("Incorrect password. Please try again."); passwordInputBox.Text(""); class=class="str">"cmt">// Clear the password input } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle close button click for authentication | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnCloseAuthButtonClick() { authentication.Destroy(); ExpertRemove(); class=class="str">"cmt">// Remove the expert if user closes the authentication dialog Print("Authentication dialog closed."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create necessary UI controls | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateControls() { class="type">long chart_id = ChartID(); class=class="str">"cmt">// Create the input box if (!inputBox.Create(chart_id, "InputBox", class="num">0, class="num">5, class="num">25, class="num">460, class="num">95)) { Print("Failed to create input box"); class="kw">return false; } adminPanel.Add(inputBox); class=class="str">"cmt">// Character counter if (!charCounter.Create(chart_id, "CharCounter", class="num">0, class="num">380, class="num">5, class="num">460, class="num">25)) { Print("Failed to create character counter"); class="kw">return false; } charCounter.Text("class="num">0/" + IntegerToString(MAX_MESSAGE_LENGTH)); adminPanel.Add(charCounter); class=class="str">"cmt">// Clear button if (!clearButton.Create(chart_id, "ClearButton", class="num">0, class="num">235, class="num">95, class="num">345, class="num">125)) { Print("Failed to create clear button"); class="kw">return false; } clearButton.Text("Clear"); adminPanel.Add(clearButton); class=class="str">"cmt">// Send button if (!sendButton.Create(chart_id, "SendButton", class="num">0, class="num">350, class="num">95, class="num">460, class="num">125)) { Print("Failed to create send button"); class="kw">return false; } sendButton.Text("Send"); adminPanel.Add(sendButton); class=class="str">"cmt">// Change font button
「面板按钮的坐标与创建兜底」
在 MT5 自定义面板里堆按钮,最容易翻车的不是画法,而是 Create 失败没兜底直接崩脚本。下面这段把字体切换、主题切换、最小化/最大化/关闭以及快捷消息入口一次性挂到 adminPanel 上,每个 Create 都带失败打印并返回 false,开 MT5 跑之前先确认 chart_id 传的是当前图表常量。 注意几个硬坐标:changeFontButton 占 (95,95)-(230,115),toggleThemeButton 在 (5,95)-(90,115),两者 Y 起点都是 95 像素。最小化到关闭三个按钮的 Y 写成了 -22,注释里明说是为了可见性做了调整,说明原面板顶部可能被遮挡,你抄代码时若面板位置不同,这负数 Y 可能让按钮飞出可见区。 快捷消息区用固定 8 格数组 quickMessages[8],从 (5,160) 起排,单键宽 222、高 65、间距 5。算一下:8 个按钮纵向占 160 + 8×65 + 7×5 = 875 像素,普通 1080p 图表下方空间可能不够,倾向在小于 900 高度窗口里只显示前 6 个。外汇与贵金属图表叠加此类 GUI 存在刷新抢占风险,参数请先在模拟盘验证。
if (!changeFontButton.Create(chart_id, "ChangeFontButton", class="num">0, class="num">95, class="num">95, class="num">230, class="num">115)) { Print("Failed to create change font button"); class="kw">return false; } changeFontButton.Text("Font<>"); adminPanel.Add(changeFontButton); class=class="str">"cmt">// Toggle theme button if (!toggleThemeButton.Create(chart_id, "ToggleThemeButton", class="num">0, class="num">5, class="num">95, class="num">90, class="num">115)) { Print("Failed to create toggle theme button"); class="kw">return false; } toggleThemeButton.Text("Theme<>"); adminPanel.Add(toggleThemeButton); class=class="str">"cmt">// Minimize button if (!minimizeButton.Create(chart_id, "MinimizeButton", class="num">0, class="num">375, -class="num">22, class="num">405, class="num">0)) class=class="str">"cmt">// Adjusted Y-coordinate for visibility { Print("Failed to create minimize button"); class="kw">return false; } minimizeButton.Text("_"); adminPanel.Add(minimizeButton); class=class="str">"cmt">// Maximize button if (!maximizeButton.Create(chart_id, "MaximizeButton", class="num">0, class="num">405, -class="num">22, class="num">435, class="num">0)) class=class="str">"cmt">// Adjusted Y-coordinate for visibility { Print("Failed to create maximize button"); class="kw">return false; } maximizeButton.Text("[ ]"); adminPanel.Add(maximizeButton); class=class="str">"cmt">// Close button for admin panel if (!closeButton.Create(chart_id, "CloseButton", class="num">0, class="num">435, -class="num">22, class="num">465, class="num">0)) class=class="str">"cmt">// Adjusted Y-coordinate for visibility { Print("Failed to create close button"); class="kw">return false; } closeButton.Text("X"); adminPanel.Add(closeButton); class=class="str">"cmt">// Quick messages class="kw">return CreateQuickMessageButtons(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create quick message buttons | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateQuickMessageButtons() { class="type">class="kw">string quickMessages[class="num">8] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; class="type">int startX = class="num">5, startY = class="num">160, width = class="num">222, height = class="num">65, spacing = class="num">5; for (class="type">int i = class="num">0; i < class="num">8; i++) {
◍ 面板销毁与消息发送的收尾钩子
这段收尾代码把管理面板的生命周期和交互回调钉死了:OnDeinit 里直接调 adminPanel.Destroy() 并打一行 Deinitialization complete,EA 卸载或图表关闭时图形对象不会残留,避免下次加载叠出幽灵按钮。 发消息的逻辑分两块:OnSendButtonClick 先读 inputBox.Text(),空串就报 No message entered.,非空才走 SendMessageToTelegram;OnQuickMessageButtonClick 则按 index 从长度为 8 的 quickMessages 数组取预设短语直发。注意 quickMessages 写死 8 个元素,和前面循环创建的按钮数必须一致,改了 QuickMessage1~8 之外的数量要同步调数组大小。 回调里所有结果只走 Print,不弹窗不写文件。真要拿来做盯盘告警,建议在 SendMessageToTelegram 失败分支加 ChartIndicatorDelete 或 MessageBeep 之类的本地兜底,否则 MT5 日志外你感知不到推送断没断。外汇与贵金属行情跳空频繁,这类面板只管通知、不替你下单,实盘前务必在策略测试器里跑一遍注销流程验证对象回收。
if (!quickMessageButtons[i].Create(ChartID(), "QuickMessageButton" + IntegerToString(i + class="num">1), class="num">0, startX + (i % class="num">2) * (width + spacing), startY + (i / class="num">2) * (height + spacing), startX + (i % class="num">2) * (width + spacing) + width, startY + (i / class="num">2) * (height + spacing) + height)) { Print("Failed to create quick message button ", i + class="num">1); class="kw">return false; } quickMessageButtons[i].Text(quickMessages[i]); adminPanel.Add(quickMessageButtons[i]); } class="kw">return true; } 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">//| Handle custom message send button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnSendButtonClick() { class="type">class="kw">string message = inputBox.Text(); if (message != "") { if (SendMessageToTelegram(message)) Print("Custom message sent: ", message); else Print("Failed to send custom message."); } else { Print("No message entered."); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle clear button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnClearButtonClick() { inputBox.Text(""); OnInputChange(); Print("Input box cleared."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle quick message button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnQuickMessageButtonClick(class="type">int index) { class="type">class="kw">string quickMessages[class="num">8] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 }; class="type">class="kw">string message = quickMessages[index]; if (SendMessageToTelegram(message)) Print("Quick message sent: ", message); else Print("Failed to send quick message."); } class=class="str">"cmt">//+------------------------------------------------------------------+