在MQL5中创建交易管理员面板(第四部分):登录安全层·进阶篇
🔐

在MQL5中创建交易管理员面板(第四部分):登录安全层·进阶篇

(2/3)· 面板裸奔等于把广播社区的话筒交给陌生人,本篇用密码层堵住这个口子

进阶 第 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" 只是演示用,挂实盘前务必改成随机串,否则别人开你终端就能发指令。外汇和贵金属波动剧烈,这类面板只管发信号,下单风险仍由你扛。

MQL5 / C++
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() 才会立刻重绘,否则可能要等下一次事件才露面。 外汇与贵金属品种波动剧烈、杠杆风险高,这类本地弹窗只做界面演示,密码明文写在客户端代码里并不安全,实盘别直接照搬。

MQL5 / C++
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 日志自查一次。

MQL5 / C++
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 仍可能偷偷跑。

MQL5 / C++
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 存在刷新抢占风险,参数请先在模拟盘验证。

MQL5 / C++
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 日志外你感知不到推送断没断。外汇与贵金属行情跳空频繁,这类面板只管通知、不替你下单,实盘前务必在策略测试器里跑一遍注销流程验证对象回收。

MQL5 / C++
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">//+------------------------------------------------------------------+
把面板巡检交给小布盯盘
这些登录层的异常访问和未授权操作特征,小布盯盘的 AIGC 已内置识别,打开对应品种页就能看到告警,你只管决定要不要改密码策略。

常见问题

EX5加密保护的是代码知识产权,防止反编译;面板密码层限制的是运行时的功能访问,两者防护对象不同,缺一个都不算闭环。
把密码明文写死在代码里或存在本地未加密文件,等于没防。进阶做法应做哈希校验并隔离内部功能入口。
可以,小布盯盘的品种页会标记异常登录与越权操作倾向,省去你手动翻日志的重复劳动。
启动阶段做令牌校验与发送权限隔离,避免面板被绕过后直接调API群发,具体实现见本篇第五、六节拆解。
管用但需重构入口,密码层会先拦截非授权会话,再放行原有广播逻辑,完整衔接见同系列第三篇。