在MQL5中创建交易管理员面板(第四部分):登录安全层·综合运用
(3/3)· 从编译加密到面板密码,收尾篇把未授权访问与误发消息的风险压到最低
输入框计数与面板换肤的事件响应
在 MT5 自定义面板里,输入框内容变化时要实时刷新字数统计。OnInputChange 先取 inputBox 的文本长度,再拼成「当前/上限」格式写回 charCounter,最后 ChartRedraw 强制重绘,避免界面卡帧。 暗色主题的切换由 OnToggleThemeButtonClick 触发:把 darkTheme 布尔值取反后调用 UpdateThemeColors,并用 Print 输出当前是 Dark 还是 Light,方便在专家日志里确认状态翻转。 UpdateThemeColors 是换肤核心。它先调 adminPanel.UpdateThemeColors 做占位,再按 darkTheme 给文本、按钮底、边框、背景分别指派颜色——例如暗色用 clrWhite 文本配 clrDarkBlue 背景,亮色则用 clrBlack 配 clrWhite。随后对 8 个固定按钮加 quickMessageButtons 数组逐一调 UpdateButtonTheme,最后改 charCounter 颜色并重绘。 UpdateButtonTheme 只是三层 Set 的封装:文本色、背景色、边框色。把循环里的按钮和独立按钮都走同一函数,能让主题切换的代码量从几十条收敛到一次调用。 字体按钮 OnChangeFontButtonClick 用取模轮询 availableFonts 数组,index 自增后赋给 inputBox.Font,这样每点一次就切到下一种字体,不会越界。
class="type">void OnInputChange() { class="type">int currentLength = StringLen(inputBox.Text()); charCounter.Text(IntegerToString(currentLength) + "/" + IntegerToString(MAX_MESSAGE_LENGTH)); ChartRedraw(); } class="type">void OnToggleThemeButtonClick() { darkTheme = !darkTheme; UpdateThemeColors(); Print("Theme toggled: ", darkTheme ? "Dark" : "Light"); } class="type">void UpdateThemeColors() { class=class="str">"cmt">// Use the dialog&class="macro">#x27;s theme update method as a placeholder. adminPanel.UpdateThemeColors(darkTheme); class="type">class="kw">color textColor = darkTheme ? clrWhite : clrBlack; class="type">class="kw">color buttonBgColor = darkTheme ? clrDarkSlateGray : clrGainsboro; class="type">class="kw">color borderColor = darkTheme ? clrSlateGray : clrGray; class="type">class="kw">color bgColor = darkTheme ? clrDarkBlue : clrWhite; inputBox.SetTextColor(textColor); inputBox.SetBackgroundColor(bgColor); inputBox.SetBorderColor(borderColor); UpdateButtonTheme(clearButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(sendButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(toggleThemeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(changeFontButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(minimizeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(maximizeButton, textColor, buttonBgColor, borderColor); UpdateButtonTheme(closeButton, textColor, buttonBgColor, borderColor); for (class="type">int i = class="num">0; i < ArraySize(quickMessageButtons); i++) { UpdateButtonTheme(quickMessageButtons[i], textColor, buttonBgColor, borderColor); } charCounter.Color(textColor); ChartRedraw(); } class="type">void UpdateButtonTheme(CButton &button, class="type">class="kw">color textColor, class="type">class="kw">color bgColor, class="type">class="kw">color borderColor) { button.SetTextColor(textColor); button.SetBackgroundColor(bgColor); button.SetBorderColor(borderColor); } class="type">void OnChangeFontButtonClick() { currentFontIndex = (currentFontIndex + class="num">1) % ArraySize(availableFonts); inputBox.Font(availableFonts[currentFontIndex]);
「面板收展与 Telegram 推送的底层回调」
这段逻辑管的是 MT5 自定义面板的最小化、最大化、关闭,以及把消息发到 Telegram 机器人。注意 OnMinimizeButtonClick 里把 minimized 置 true 后只 Hide 了 adminPanel 和 minimizeButton,同时 Show maximizeButton 与 closeButton——也就是说面板收起后并非销毁对象,只是隐藏,内存里的控件句柄都还在。 OnMaximizeButtonClick 用 if(minimized) 做闸门,避免重复展开;展开时把 closeButton 重新 Hide,说明关闭键只在最小化态暴露,正常展开时不应误触 EA 卸载。OnCloseButtonClick 直接调 ExpertRemove(),会终止整个 EA 实例,外汇/贵金属实盘挂这个面板时误点可能倾向直接脱钩当前图表策略,风险不低。 SendMessageToTelegram 拼的是标准 Telegram Bot API:url 用 InputBotToken 和 /sendMessage 端点,jsonMessage 手写 chat_id 与 text。WebRequest 超时写死 5000 毫秒,返回码 ==200 才认作发送成功。开 MT5 后把 InputBotToken、InputChatId 填进 EA 输入项,即可用面板按钮验证推送是否进群。
clearButton.Font(availableFonts[currentFontIndex]); sendButton.Font(availableFonts[currentFontIndex]); toggleThemeButton.Font(availableFonts[currentFontIndex]); changeFontButton.Font(availableFonts[currentFontIndex]); for (class="type">int i = class="num">0; i < ArraySize(quickMessageButtons); i++) { quickMessageButtons[i].Font(availableFonts[currentFontIndex]); } Print("Font changed to: ", availableFonts[currentFontIndex]); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle minimize button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnMinimizeButtonClick() { minimized = true; adminPanel.Hide(); minimizeButton.Hide(); maximizeButton.Show(); closeButton.Show(); Print("Panel minimized."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle maximize button click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnMaximizeButtonClick() { if (minimized) { adminPanel.Show(); minimizeButton.Show(); maximizeButton.Hide(); closeButton.Hide(); Print("Panel maximized."); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle close button click for admin panel | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnCloseButtonClick() { ExpertRemove(); Print("Admin Panel closed."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Send the message to Telegram | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool SendMessageToTelegram(class="type">class="kw">string message) { class="type">class="kw">string url = "https:class=class="str">"cmt">//api.telegram.org/bot" + InputBotToken + "/sendMessage"; class="type">class="kw">string jsonMessage = "{\"chat_id\":\"" + InputChatId + "\", \"text\":\"" + message + "\"}"; class="type">char post_data[]; ArrayResize(post_data, StringToCharArray(jsonMessage, post_data, class="num">0, WHOLE_ARRAY) - class="num">1); class="type">int timeout = class="num">5000; class="type">char result[]; class="type">class="kw">string responseHeaders; class="type">int res = WebRequest("POST", url, "Content-Type: application/json\r\n", timeout, post_data, result, responseHeaders); if (res == class="num">200) { Print("Message sent successfully: ", message); class="kw">return true; } else {
◍ 把推送失败的错误码打出来
在 MT5 里调 WebRequest 往外部接口推信号,最怕静默失败。上面这段是请求返回非 200 时的收尾处理,直接把 HTTP 状态码和终端错误码打印到日志,方便你当场定位是网关拒了还是证书没配。 第一行用 Print 拼接出可读信息:res 是 WebRequest 返回的 HTTP 代码,GetLastError 拿的是 MQL5 内部错误号,两者一起看才分得清是网络层还是环境层出的问题。第二行把 result 字符数组转成字符串原样输出,接口若回了 JSON 报错体也能看见。 最后 return false 让调用方知道本次发送没成功,后续逻辑可以走本地缓存或重试。外汇与贵金属行情推送对时效敏感,这类错误日志建议每次都留,实盘前先在策略测试器里故意填错 URL 跑一遍验证输出。
Print("Failed to send message. HTTP code: ", res, " Error code: ", GetLastError()); Print("Response: ", CharArrayToString(result)); class="kw">return false; } }
PIN 校验未过前功能全锁
编译通过的 EA 在 MT5 里跑起来后,只要面板输入的 PIN 码不匹配,所有管理类按钮和交易触发入口都处于禁用状态,普通用户点不动。这一层拦截在 OnInit 阶段就生效,不是靠隐藏窗口实现,而是直接把控件 enabled 属性置 false。 目前这版防护只能挡住不会反编译的玩家。用常规 hex 编辑器或反汇编工具把 EX4 拖出来,PIN 字符串常量大概率明文躺在段里,熟练者改个跳转就能绕过。所以别把它当成品级授权,只是演示「输入校验 → 功能解锁」的最小闭环。 下阶段要做的不是加长 PIN,而是把校验逻辑移到服务器端或做哈希比对,让本地二进制里看不到原值。外汇与贵金属杠杆高,任何跟单或管理面板若被未授权调用,可能造成账户级风险,这类加固得在实盘前完成。
「把工具请下神坛」
给管理员面板加登录校验,本质是把硬编码明文密码挡在关键操作前,编译后 MQL5 的反反编译机制再叠一层,外部没源码基本进不去。这套组合在本地脚本里够用,但硬编码凭证仍是薄弱点,后续若接多重认证才更接近生产级。 外汇和贵金属杠杆高、波动猛,这类面板只管权限不管行情,实盘前务必在策略测试器里跑通逻辑。代码编译完拿不到源就难逆向,这点确实省心,但别神话它——它只是降低未授权改动的概率,不替你扛穿仓风险。 想验证就下那 18.66 KB 的 mq5 自己编译,改个全局密码看面板反应,比看十篇说明来得直接。