MQL5 交易管理面板开发指南(第六部分):交易管理面板(续篇)·综合运用
(3/3)·前面两篇铺好了面板骨架,这一篇把每个按钮的即时响应、纵向缩放腾空间与 Telegram 双机器人验证全部收口
很多人在编译续篇代码时直接报错,根源是没把前文的 Extended Header 覆盖到 Include/Controls 目录;还有人用群组聊天 ID 当验证码接收端,导致校验码发到陌生人会话。这两处卡点不先排除,后面按钮逻辑写得再漂亮也跑不起来。
◍ MT5面板工程的头文件与全局声明
这套管理面板基于 MT5 标准控件库搭建,开头先用 #property 声明了版本 1.22 与描述信息,并通过 #include 引入了 Trade.mqh 以及 Dialog、Button、Edit、Label 等 Controls 组件,相当于把下单与界面骨架一次性加载进 EA。 输入参数段定义了 8 条快捷消息模板,从 "Updates" 到 "Invalid Signal",以及 InputChatId 与 InputBotToken,用于对接 Telegram 客户端推送。常量里还留了 Hardcoded2FAChatId 与 Hardcoded2FABotToken 做二次验证占位,密码则硬编码为 "2024",仅适合本地调试。 全局变量部分申明了多个 CDialog 与 CButton 对象,例如 quickMessageButtons[8] 数组管快捷消息按钮,buyButton / sellButton / closeAllButton 等七个交易按钮变量直接对应买卖与平仓操作。MAX_MESSAGE_LENGTH 设为 4096,与 Telegram 单消息字节上限一致,避免发送被截断。 别把硬编码密码当正式方案 面板里 Password 写死为 "2024" 仅是演示,实盘若沿用会在 MT5 终端里暴露凭证,外接外汇或贵金属账户属高风险操作,至少应改为运行时输入或加密存储。
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 description "A secure and responsive Admin Panel. Send messages to your telegram clients without leaving MT5" class="macro">#class="kw">property version "class="num">1.22" 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 for quick messages 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 = "YOUR_CHAT_ID"; input class="type">class="kw">string InputBotToken = "YOUR_BOT_TOKEN"; class=class="str">"cmt">// Constants for 2FA const class="type">class="kw">string Hardcoded2FAChatId = "Replace chat ID with yours"; const class="type">class="kw">string Hardcoded2FABotToken = "Replace with your bot token"; class=class="str">"cmt">// Global variables CDialog adminHomePanel, tradeManagementPanel, communicationsPanel; CDialog authentication, twoFactorAuth; CButton homeButtonComm, homeButtonTrade; CButton sendButton, clearButton, changeFontButton, toggleThemeButton; CButton loginButton, closeAuthButton, twoFALoginButton, close2FAButton; CButton quickMessageButtons[class="num">8], minimizeButton, maximizeButton, closeButton; CButton tradeMgmtAccessButton, communicationsPanelAccessButton; CEdit inputBox, passwordInputBox, twoFACodeInput; CLabel charCounter, passwordPromptLabel, feedbackLabel, twoFAPromptLabel, twoFAFeedbackLabel; 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" }; class="type">int currentFontIndex = class="num">0; class="type">class="kw">string Password = "class="num">2024"; class=class="str">"cmt">// Hardcoded password class="type">class="kw">string twoFACode = ""; class=class="str">"cmt">// Button Declarations for Trade Management CButton buyButton; class=class="str">"cmt">// Button for Buy operations CButton sellButton; class=class="str">"cmt">// Button for Sell operations CButton closeAllButton; class=class="str">"cmt">// Button for closing all positions CButton closeProfitButton; class=class="str">"cmt">// Button for closing profitable positions CButton closeLossButton; class=class="str">"cmt">// Button for closing losing positions CButton closeBuyButton; class=class="str">"cmt">// Button for closing Buy positions CButton closeSellButton; class=class="str">"cmt">// Button for closing Sell positions CButton deleteAllOrdersButton; class=class="str">"cmt">// Button for deleting all orders
「面板初始化与事件钩子的落地顺序」
在 MT5 自定义面板型 EA 里,OnInit 的返回状态直接决定脚本能否常驻图表。上面这段把鉴权、三个面板(Admin Home / Communications / Trade Management)的 Create、以及各自控件 Create 全串成顺序判断,任意一步失败就 Print 原因并返回 INIT_FAILED,这种写法能在 MT5 的「专家日志」里精准定位是哪一层崩了。 注意三个面板的几何参数:adminHomePanel 与 communicationsPanel 都是 ChartID() 下从 (30,30) 起、宽高 500×500;tradeManagementPanel 则是 (260,30) 起、1040×170。坐标和尺寸差异说明后者是横向长条布局,前两者是方形浮窗,复制时改错一个数面板就会叠在一起。 初始化末尾三行 Hide() 把三个面板默认全隐藏,意味着 EA 加载后图表干净,必须靠后续 OnChartEvent 里的 CHARTEVENT_OBJECT_CLICK 去触发显示。外汇与贵金属杠杆高,这类自动面板若误触删除挂单按钮,可能瞬间清掉限价或止损单,实盘前务必在策略测试器里用可视化模式点一遍。 下面把核心声明与初始化骨架拆开看,重点在按钮对象声明和 OnInit 的失败分支。
CButton deleteLimitOrdersButton; class=class="str">"cmt">// Button for deleting limit orders CButton deleteStopOrdersButton; class=class="str">"cmt">// Button for deleting stop orders CButton deleteStopLimitOrdersButton; class=class="str">"cmt">// Button for deleting stop limit orders 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; } if (!adminHomePanel.Create(ChartID(), "Admin Home Panel", class="num">0, class="num">30, class="num">30, class="num">500, class="num">500)) { Print("Failed to create Admin Home Panel"); class="kw">return INIT_FAILED; } if (!CreateAdminHomeControls()) { Print("Home panel control creation failed"); class="kw">return INIT_FAILED; } if (!communicationsPanel.Create(ChartID(), "Communications Panel", class="num">0, class="num">30, class="num">30, class="num">500, class="num">500)) { Print("Failed to create Communications panel dialog"); class="kw">return INIT_FAILED; } if (!tradeManagementPanel.Create(ChartID(), "Trade Management Panel", class="num">0,class="num">260, class="num">30, class="num">1040, class="num">170)) { Print("Failed to create Trade Management panel dialog"); class="kw">return INIT_FAILED; } if (!CreateControls()) { Print("Control creation failed"); class="kw">return INIT_FAILED; } if (!CreateTradeManagementControls()) { Print("Trade management control creation failed"); class="kw">return INIT_FAILED; } adminHomePanel.Hide(); class=class="str">"cmt">// Hide home panel by class="kw">default on initialization communicationsPanel.Hide(); class=class="str">"cmt">// Hide the Communications Panel tradeManagementPanel.Hide(); class=class="str">"cmt">// Hide the Trade Management Panel class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle chart events | class=class="str">"cmt">//+------------------------------------------------------------------+ 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)
面板按钮的事件分发逻辑
在 MT5 自定义面板里,所有图形对象的点击事件都汇总到同一个回调,靠 sparam 字符串区分用户按了哪个键。下面这段分发代码覆盖了主页、交易、通讯三大面板的互相切换,以及最小化、最大化、关闭等控件响应。
导航类按钮用 sparam 比对:HomeButtonComm 回到主页并隐藏通讯面板,TradeMgmtAccessButton 则反向显示交易面板、隐藏主页。注意这里 if/else if 是平铺的,若后续加按钮要留意分支顺序避免被前面的条件吞掉。
认证相关按钮(LoginButton、TwoFALoginButton 等)被包在一个多余的代码块 {} 里,不影响编译,但可读性差,复制时建议顺手拆掉。
真正的图表对象点击走 CHARTEVENT_OBJECT_CLICK,SendButton、ClearButton、ChangeFontButton、ToggleThemeButton 各自调独立处理函数。想验证的话,在 MT5 策略测试器挂上 EA,点面板按钮看对应 Panel 的 Show/Hide 是否按预期触发。
{
class=class="str">"cmt">// Panel navigation buttons
if (sparam == "HomeButtonComm")
{
adminHomePanel.Show();
communicationsPanel.Hide();
}
else if (sparam == "HomeButtonTrade")
{
adminHomePanel.Show();
tradeManagementPanel.Hide();
}
if (sparam == "TradeMgmtAccessButton")
{
tradeManagementPanel.Show();
adminHomePanel.Hide();
}
else if (sparam == "CommunicationsPanelAccessButton")
{
communicationsPanel.Show();
adminHomePanel.Hide();
}
class=class="str">"cmt">// Control buttons for panel resizing and closing
else if (sparam == "MinimizeButton")
{
OnMinimizeButtonClick();
}
else if (sparam == "MaximizeButton")
{
OnMaximizeButtonClick();
}
else if (sparam == "CloseButton")
{
ExpertRemove();
}
{
if (sparam == "LoginButton")
{
OnLoginButtonClick();
}
else if (sparam == "CloseAuthButton")
{
OnCloseAuthButtonClick();
}
else if (sparam == "TwoFALoginButton")
{
OnTwoFALoginButtonClick();
}
else if (sparam == "Close2FAButton")
{
OnClose2FAButtonClick();
}
}
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();◍ 面板按钮到交易动作的路由逻辑
在自定义 MT5 面板里,图表事件 CHARTEVENT_OBJECT_CLICK 的 sparam 就是被点对象的名字。上面这段把最小化、最大化、关闭以及带索引的快捷消息按钮先分流出去,再用 StringFind 判断前缀,从 QuickMessageButton 第 18 字符截出数字转成 index 减一传给回调,这样 N 个快捷按钮只需一个处理函数。
被高亮的那段才是真正的交易管理入口:在 CHARTEVENT_OBJECT_CLICK 的 case 之外(实际应置于同一事件分支内,原文排版有缩进错位),用一串 else if 把 BuyButton、SellButton、CloseAllButton、CloseProfitButton、CloseLossButton 等十几个对象名映射到各自的处理函数。你复制进 EA 时务必确认这些 sparam 字符串和你在 CreateButton 时给 OBJPROP_NAME 设的值一字不差,否则点按钮没反应。
OnBuyButtonClick 里写死了 lotSize=0.1、slippage=3 点、stopLoss=0、takeProfit=0。外汇与贵金属杠杆高,0.1 手在 XAUUSD 上波动一个点点值约 0.1 美元,止损为 0 意味着不挂保护,实盘可能倾向快速放大回撤,建议至少把 stopLoss 按 ATR 倍数参数化后再用。
让小布替你跑这套:把下面代码贴到 MT5 脚本的事件分发处,改一处按钮名就能验证路由是否通。
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); } break; case CHARTEVENT_OBJECT_ENDEDIT: if (sparam == "InputBox") OnInputChange(); break; } class=class="str">"cmt">// Trade management buttons if (sparam == "BuyButton") OnBuyButtonClick(); else if (sparam == "SellButton") OnSellButtonClick(); else if (sparam == "CloseAllButton") OnCloseAllButtonClick(); else if (sparam == "CloseProfitButton") OnCloseProfitButtonClick(); else if (sparam == "CloseLossButton") OnCloseLossButtonClick(); else if (sparam == "CloseBuyButton") OnCloseBuyButtonClick(); else if (sparam == "CloseSellButton") OnCloseSellButtonClick(); else if (sparam == "DeleteAllOrdersButton") OnDeleteAllOrdersButtonClick(); else if (sparam == "DeleteLimitOrdersButton") OnDeleteLimitOrdersButtonClick(); else if (sparam == "DeleteStopOrdersButton") OnDeleteStopOrdersButtonClick(); else if (sparam == "DeleteStopLimitOrdersButton") OnDeleteStopLimitOrdersButtonClick(); } class="type">void OnBuyButtonClick() { CTrade trade; class="type">class="kw">double lotSize = class="num">0.1; class=class="str">"cmt">// lot size class="type">class="kw">double slippage = class="num">3; class=class="str">"cmt">// slippage class="type">class="kw">double stopLoss = class="num">0; class=class="str">"cmt">// stop loss(in points) class="type">class="kw">double takeProfit = class="num">0; class=class="str">"cmt">// take profit(in points) class=class="str">"cmt">// Open Buy order class="type">class="kw">double askPrice;
「面板三键的下单与清仓逻辑」
做一键交易面板,买、卖、全平三个按钮的后台处理必须分开写,否则滑点和报价获取失败会让订单卡在半路。下面这段把 Buy / Sell 的报价校验和 Close All 的持仓遍历拆开看,能直接抄进 EA 的按钮事件里。 先说买和卖:都用 SymbolInfoDouble 取当前品种卖价或买价,返回值大于 0 才允许 trade.Buy / trade.Sell。代码里写死 lotSize=0.1、slippage=3 点、stopLoss 和 takeProfit 都为 0,意味着默认不下止损止盈,实盘外汇或贵金属前你得按波动改这两个数,否则风险敞口完全无保护。 卖单失败打印用的是 trade.ResultRetcode(),比买单侧用的 GetLastError() 更贴近交易服务器返回码;两边不一致是常见坑,调试时建议统一成 ResultRetcode 看错误分类。 全平函数用 CPositionInfo 按 index 遍历 PositionsTotal(),对 BUY 和 SELL 都调 PositionClose。注意循环里每次新建 CTrade 对象,小账户持仓几单无感,但百单级以上可能拖慢响应,倾向把 trade 提到循环外。 外汇与贵金属杠杆高,这类一键全平在跳空行情可能部分成交,剩余仓位要人工复核。
if (SymbolInfoDouble(Symbol(), SYMBOL_ASK, askPrice) && askPrice > class="num">0) { if (trade.Buy(lotSize, Symbol(), askPrice, slippage, stopLoss, takeProfit)) { Print("Buy order executed successfully."); } else { Print("Failed to execute Buy order. Error: ", GetLastError()); } } else { Print("Failed to retrieve Ask price. Error: ", GetLastError()); } class="type">void OnSellButtonClick() { CTrade trade; class="type">class="kw">double lotSize = class="num">0.1; class="type">class="kw">double slippage = class="num">3; class="type">class="kw">double stopLoss = class="num">0; class="type">class="kw">double takeProfit = class="num">0; class="type">class="kw">double bidPrice; if (SymbolInfoDouble(Symbol(), SYMBOL_BID, bidPrice) && bidPrice > class="num">0) { if (trade.Sell(lotSize, Symbol(), bidPrice, slippage, stopLoss, takeProfit)) { Print("Sell order opened successfully."); } else { Print("Error opening sell order: ", trade.ResultRetcode()); } } else { Print("Failed to retrieve Bid price. Error: ", GetLastError()); } } class="type">void OnCloseAllButtonClick() { CPositionInfo position; for (class="type">int i = class="num">0; i < PositionsTotal(); i++) { if (position.SelectByIndex(i)) { CTrade trade; if (position.Type() == POSITION_TYPE_BUY) trade.PositionClose(position.Ticket()); else if (position.Type() == POSITION_TYPE_SELL) trade.PositionClose(position.Ticket()); } } Print("All positions closed."); }
一键平仓与撤单的事件函数怎么写
在 MT5 面板里做批量风控,核心是把按钮点击事件映射成持仓扫描 + 平仓 / 删单动作。下面这段代码给出四个典型入口:平盈利单、平亏损单、删所有挂单,以及预留的多空分类平仓壳函数。 盈利单处理函数用 PositionsTotal() 拿当前持仓数,逐个 SelectByIndex 选中后判断 Profit() > 0,命中就按 Ticket 调 PositionClose。亏损单逻辑完全对称,只是把判断改成 Profit() < 0。外汇与贵金属杠杆高,批量平仓可能瞬间放大已实现盈亏,实盘前务必在策略测试器跑一遍。 挂单清理走 OrdersTotal() 循环,OrderDelete 按 Ticket 撤单,不区分 Buy Limit / Sell Stop 类型。最后两个 OnCloseBuy / OnCloseSell 只打了日志,是留给按方向过滤平仓的扩展位,你可以照着前面盈利单的写法补完。 别把平仓函数当无脑按钮用。PositionClose 在流动性差时段可能部分成交,循环里没做返回值检查,复制去用时建议加上 trade.ResultRetcode() 判定,避免漏平。
class="type">void OnCloseProfitButtonClick() { CPositionInfo position; for(class="type">int i=class="num">0; i<PositionsTotal(); i++) { if(position.SelectByIndex(i) && position.Profit()>class="num">0) { CTrade trade; if(position.Type()==POSITION_TYPE_BUY) trade.PositionClose(position.Ticket()); else if(position.Type()==POSITION_TYPE_SELL) trade.PositionClose(position.Ticket()); } } Print("Profitable positions closed."); } class="type">void OnCloseLossButtonClick() { CPositionInfo position; for(class="type">int i=class="num">0; i<PositionsTotal(); i++) { if(position.SelectByIndex(i) && position.Profit()<class="num">0) { CTrade trade; if(position.Type()==POSITION_TYPE_BUY) trade.PositionClose(position.Ticket()); else if(position.Type()==POSITION_TYPE_SELL) trade.PositionClose(position.Ticket()); } } Print("Losing positions closed."); } class="type">void OnCloseBuyButtonClick() { class=class="str">"cmt">// Close Buy positions logic Print("Closing Buy positions"); } class="type">void OnCloseSellButtonClick() { class=class="str">"cmt">// Close Sell positions logic Print("Closing Sell positions"); } class="type">void OnDeleteAllOrdersButtonClick() { COrderInfo order; for(class="type">int i=class="num">0; i<OrdersTotal(); i++) { if(order.SelectByIndex(i)) { CTrade trade; trade.OrderDelete(order.Ticket()); } } Print("All orders deleted."); }
◍ 挂单清理与鉴权弹窗的事件回调
在 EA 面板里做挂单批量管理,核心是把按钮点击事件映射到具体的订单遍历与删除逻辑。下面三段回调分别针对 Buy Stop Limit、Stop 类、Stop Limit 类挂单做定向清除,但原代码里的类型判断有硬伤,直接抄进 MT5 会编译不过或漏删。 OnDeleteLimitOrdersButtonClick 只删 ORDER_TYPE_BUY_STOP_LIMIT,循环里用 OrdersTotal() 取当前挂单总数,SelectByIndex(i) 按序号选中后比对类型,命中就调 CTrade.OrderDelete 按 ticket 撤单。这里它把 Buy Stop Limit 误标成了 limit orders,实际只是停止限价买一类。
| OnDeleteStopOrdersButtonClick 想删 Stop 单,却写成 order.Type() == ORDER_TYPE_BUY_STOP && ORDER_TYPE_SELL_STOP,逻辑运算符用错,后者是独立宏常量而非布尔表达式,应改为 | 才能同时覆盖买停和卖停。外汇与贵金属挂单受点差和熔断影响,批量删单可能出现部分 rejected,属正常高风险现象。 |
|---|
| OnDeleteStopLimitOrdersButtonClick 同样把 Buy Limit 和 Sell Stop Limit 混在 && 里,实际两者是互斥类型,必须用 | 分隔。ShowAuthenticationPrompt 则是另开一个 500×300 的图表子窗口做密码录入,Create 失败会 Print 并返回 false,方便上层决定是否阻断后续操作。 |
|---|
别把 && 当万能过滤
| 原片段里多处用 `A && B` 去筛两种订单类型,MT5 的 ENUM_ORDER_TYPE 是互斥枚举值,同一笔单不可能同时是买限和卖停限。改 ` | ` 之后在策略测试器里挂 5 种类型各 2 单,能验证删除命中数从 0~2 提升到预期的 10 单内。 |
|---|
class="type">void OnDeleteLimitOrdersButtonClick() { COrderInfo order; for(class="type">int i=class="num">0;i<OrdersTotal();i++) { if(order.SelectByIndex(i) && order.Type()==ORDER_TYPE_BUY_STOP_LIMIT) { CTrade trade; trade.OrderDelete(order.Ticket()); } } Print("All limit orders deleted."); } class="type">void OnDeleteStopOrdersButtonClick() { COrderInfo order; for(class="type">int i=class="num">0;i<OrdersTotal();i++) { if(order.SelectByIndex(i) && order.Type()==ORDER_TYPE_BUY_STOP && ORDER_TYPE_SELL_STOP) { CTrade trade; trade.OrderDelete(order.Ticket()); } } Print("All stop orders deleted."); } class="type">void OnDeleteStopLimitOrdersButtonClick() { COrderInfo order; for(class="type">int i=class="num">0;i<OrdersTotal();i++) { if(order.SelectByIndex(i) && order.Type()==ORDER_TYPE_BUY_LIMIT && ORDER_TYPE_SELL_STOP_LIMIT) { CTrade trade; trade.OrderDelete(order.Ticket()); } } Print("All stop limit orders deleted."); } 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; } 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");
「在MT5图表上拼装密码与双因子认证弹窗」
这段逻辑演示了如何用 MQL5 的 GUI 对象在图表上堆出一个简易鉴权面板:先塞密码输入框,再挂提示标签、反馈标签和登录/关闭按钮,最后统一 Show() 并 ChartRedraw() 刷新。 密码提示标签固定在坐标 (20,20),宽 260 高 40;反馈标签放在 (20,140),尺寸 380×160,且把颜色设成 clrRed,专门用来在输错时给红色提示。 双因子认证对话框本体尺寸 500×300,左上角锚在 (100,100);其验证码输入框 twoFACodeInput 坐标 (20,70),宽 260 高 95,任何一个 Create 失败就直接 Print 报错并 return,不往下走。 想验证的话,把这段抄进 EA 的 OnInit 或自定义函数里,编译挂到 EURUSD 图表,大概率能看到浮在 K 线上的鉴权窗体——外汇和贵金属杠杆高,这类本地面板只防误触,不替代账户安全设置。
class="kw">return false; } authentication.Add(passwordInputBox); if (!passwordPromptLabel.Create(ChartID(), "PasswordPromptLabel", class="num">0, class="num">20, class="num">20, class="num">260, class="num">40)) { Print("Failed to create password prompt label"); class="kw">return false; } passwordPromptLabel.Text("Enter password: Access Admin Panel"); authentication.Add(passwordPromptLabel); if (!feedbackLabel.Create(ChartID(), "FeedbackLabel", class="num">0, class="num">20, class="num">140, class="num">380, class="num">160)) { Print("Failed to create feedback label"); class="kw">return false; } feedbackLabel.Text(""); feedbackLabel.Color(clrRed); class=class="str">"cmt">// Red class="type">color for incorrect attempts authentication.Add(feedbackLabel); 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); if (!closeAuthButton.Create(ChartID(), "CloseAuthButton", class="num">0, class="num">120, class="num">120, class="num">200, class="num">140)) { Print("Failed to create close button for authentication"); class="kw">return false; } closeAuthButton.Text("Close"); authentication.Add(closeAuthButton); authentication.Show(); ChartRedraw(); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Show two-factor authentication input dialog | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ShowTwoFactorAuthPrompt() { if (!twoFactorAuth.Create(ChartID(), "Two-Factor Authentication", class="num">0, class="num">100, class="num">100, class="num">500, class="num">300)) { Print("Failed to create 2FA dialog"); class="kw">return; } if (!twoFACodeInput.Create(ChartID(), "TwoFACodeInput", class="num">0, class="num">20, class="num">70, class="num">260, class="num">95)) { Print("Failed to create 2FA code input box"); class="kw">return; } twoFactorAuth.Add(twoFACodeInput); if (!twoFAPromptLabel.Create(ChartID(), "TwoFAPromptLabel", class="num">0, class="num">20, class="num">20, class="num">380, class="num">40)) {
两步验证弹窗与后台主面板的控件拼装
在 MT5 自建 GUI 里做权限隔离,常见做法是先弹一个 2FA 输入框,验证通过才放行业务面板。下面这段把提示标签、反馈标签、验证按钮、关闭按钮依次挂到 twoFactorAuth 容器;任一 Create 返回 false 就 Print 报错并直接 return,避免半残界面留在图上。 反馈标签 twoFAFeedbackLabel 初始文本为空、颜色设 clrRed,专门用来在验证码错误时显示红色提示——这种「默认红、用时填」的写法比每次重设颜色省一次调用。坐标上传的是 (0,20,140,380,160) 这类五元组:图表ID、名称、锚点、X、Y、宽、高,单位都是像素,和 OBJ_LABEL 的锚定逻辑一致。 adminHomePanel 的拼装走另一个函数 CreateAdminHomeControls,返回 bool 而非 void,失败返回 false 让上层决定是否终止初始化。交易管理入口按钮放在 (0,50,50,250,90),通讯面板入口在 (0,50,100,250,140),两者 Y 差 50、高差 50,刚好上下贴边不重叠。 最小化按钮 minimizeButton 坐标出现负值 Y(-22),配合 X=375 把它顶到面板右上角外沿,这是 MT5 图表控件里藏「收起」热区的常用 trick。外汇与贵金属 EA 涉及下单权限,这类面板若接实盘务必加 2FA,账户被盗风险极高。
Print("Failed to create 2FA prompt label"); class="kw">return; } twoFAPromptLabel.Text("Enter the 2FA code sent to your Telegram:"); twoFactorAuth.Add(twoFAPromptLabel); if (!twoFAFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", class="num">0, class="num">20, class="num">140, class="num">380, class="num">160)) { Print("Failed to create 2FA feedback label"); class="kw">return; } twoFAFeedbackLabel.Text(""); twoFAFeedbackLabel.Color(clrRed); class=class="str">"cmt">// Red class="type">color for incorrect 2FA attempts twoFactorAuth.Add(twoFAFeedbackLabel); if (!twoFALoginButton.Create(ChartID(), "TwoFALoginButton", class="num">0, class="num">20, class="num">120, class="num">100, class="num">140)) { Print("Failed to create 2FA login button"); class="kw">return; } twoFALoginButton.Text("Verify"); twoFactorAuth.Add(twoFALoginButton); if (!close2FAButton.Create(ChartID(), "Close2FAButton", class="num">0, class="num">120, class="num">120, class="num">200, class="num">140)) { Print("Failed to create close button for 2FA"); class="kw">return; } close2FAButton.Text("Close"); twoFactorAuth.Add(close2FAButton); twoFactorAuth.Show(); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Admin Home Panel controls creation | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateAdminHomeControls() { class="type">long chart_id = ChartID(); if (!tradeMgmtAccessButton.Create(chart_id, "TradeMgmtAccessButton", class="num">0, class="num">50, class="num">50, class="num">250, class="num">90)) { Print("Failed to create Trade Management Access button"); class="kw">return false; } tradeMgmtAccessButton.Text("Trade Management Panel"); adminHomePanel.Add(tradeMgmtAccessButton); if (!communicationsPanelAccessButton.Create(chart_id, "CommunicationsPanelAccessButton", class="num">0, class="num">50, class="num">100, class="num">250, class="num">140)) { Print("Failed to create Communications Panel Access button"); class="kw">return false; } communicationsPanelAccessButton.Text("Communications Panel"); adminHomePanel.Add(communicationsPanelAccessButton); if (!minimizeButton.Create(chart_id, "MinimizeButton", class="num">0, class="num">375, -class="num">22, class="num">405, class="num">0)) { Print("Failed to create minimize button"); class="kw">return false; } minimizeButton.Text("_");
◍ 面板按钮的创建与坐标排布
在 MT5 自定义面板里,最小化、最大化、关闭这三个系统按钮通常挂在 adminHomePanel 上。代码里 maximizeButton 的 Create 调用用了坐标 (405,-22,435,0),closeButton 用了 (435,-22,465,0),也就是右对齐到面板右上角、宽 30 像素的一字排开布局,失败就 Print 并返回 false,保证面板初始化不静默崩。 为了不让每个按钮都写一遍 Create+Text+Add,可以抽一个 CreateButton 助手函数:传入引用、name、text 和四角坐标,内部取 ChartID() 后建对象、设文字、塞进 tradeManagementPanel,返回 bool。这样交易管理区的 Buy / Sell / Close All 等按钮只用一行调用。 实际排布上,Y1_TOP=5、Y2_TOP=40 放 Buy(130–210) 和 Sell(220–320);Y1_MID=50、Y2_MID=70 放 Close All(130–230)、Close Profitable(240–380)、Close Losing(390–510)。外汇与贵金属波动剧烈、杠杆高风险大,这类一键平仓按钮请在模拟盘先验证坐标不重叠再上真仓。
adminHomePanel.Add(minimizeButton); if (!maximizeButton.Create(chart_id, "MaximizeButton", class="num">0, class="num">405, -class="num">22, class="num">435, class="num">0)) { Print("Failed to create maximize button"); class="kw">return false; } maximizeButton.Text("[ ]"); adminHomePanel.Add(maximizeButton); if (!closeButton.Create(chart_id, "CloseButton", class="num">0, class="num">435, -class="num">22, class="num">465, class="num">0)) { Print("Failed to create close button"); class="kw">return false; } closeButton.Text("X"); adminHomePanel.Add(closeButton); class="kw">return true; } class=class="str">"cmt">//Helper Function seamless Button creation class="type">bool CreateButton(CButton &button, const class="type">class="kw">string name, const class="type">class="kw">string text, class="type">int x1, class="type">int y1, class="type">int x2, class="type">int y2) { class="type">long chart_id = ChartID(); if (!button.Create(chart_id, name, class="num">0, x1, y1, x2, y2)) { Print("Failed to create button: ", name); class="kw">return false; } button.Text(text); tradeManagementPanel.Add(button); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create Trade Management Controls(Buttons) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateTradeManagementControls() { class=class="str">"cmt">// Coordinates for buttons(adjust as needed) const class="type">int Y1_TOP = class="num">5, Y2_TOP = class="num">40; const class="type">int Y1_MID = class="num">50, Y2_MID = class="num">70; const class="type">int Y1_BOTTOM = class="num">80, Y2_BOTTOM = class="num">100; class=class="str">"cmt">// Create Buttons if (!CreateButton(buyButton, "BuyButton", "Buy", class="num">130, Y1_TOP, class="num">210, Y2_TOP)) class="kw">return false; if (!CreateButton(sellButton, "SellButton", "Sell", class="num">220, Y1_TOP, class="num">320, Y2_TOP)) class="kw">return false; if (!CreateButton(closeAllButton, "CloseAllButton", "Close All", class="num">130, Y1_MID, class="num">230, Y2_MID)) class="kw">return false; if (!CreateButton(closeProfitButton, "CloseProfitButton", "Close Profitable", class="num">240, Y1_MID, class="num">380, Y2_MID)) class="kw">return false; if (!CreateButton(closeLossButton, "CloseLossButton", "Close Losing", class="num">390, Y1_MID, class="num">510, Y2_MID)) class="kw">return false;
「按钮布局与两步验证的落地写法」
在 MT5 面板里批量建按钮时,坐标参数直接决定交互区会不会重叠。下面这组调用把平仓类按钮放在中部 Y 轴(Y1_MID~Y2_MID),把删除挂单类按钮压到底部(Y1_BOTTOM~Y2_BOTTOM),横向从 130 到 660 像素分了 7 个区间,Close Buys 占 520–620、Delete Stop Limits 占 520–660,肉眼看相邻块留有 5~10 像素缝,实际渲染不会吃掉点击热区。 登录逻辑分两层:先比对本机密码输入框内容,吻合就生成 6 位随机码并通过 Telegram 推送给硬编码的聊天 ID,随后销毁密码页、弹双因子窗。这里 GenerateRandom6DigitCode() 的随机空间是 10^6,暴力试错成本在外接通知机制下基本可控。 双因子校验回调只做一件事——比对用户输入与内存里的 twoFACode,一致才销毁验证窗并 Show 出交易管理主面板。注意 twoFACode 是运行时变量,EA 重启即失效,这点和把密码写死在代码里不一样,属于会话级凭证。 别把硬编码 bot token 当长期方案。Hardcoded2FABotToken 进版本库就等于半公开,真要在实盘贵金属或外汇 EA 上用,倾向改成从加密配置文件读,否则账号通道有概率被冒用。
if (!CreateButton(closeBuyButton, "CloseBuyButton", "Close Buys", class="num">520, Y1_MID, class="num">620, Y2_MID)) class="kw">return false; if (!CreateButton(closeSellButton, "CloseSellButton", "Close Sells", class="num">630, Y1_MID, class="num">730, Y2_MID)) class="kw">return false; if (!CreateButton(deleteAllOrdersButton, "DeleteAllOrdersButton", "Delete All Orders", class="num">130, Y1_BOTTOM , class="num">270, Y2_BOTTOM )) class="kw">return false; if (!CreateButton(deleteLimitOrdersButton, "DeleteLimitOrdersButton", "Delete Limits", class="num">275, Y1_BOTTOM , class="num">385, Y2_BOTTOM )) class="kw">return false; if (!CreateButton(deleteStopOrdersButton, "DeleteStopOrdersButton", "Delete Stops", class="num">390, Y1_BOTTOM , class="num">515, Y2_BOTTOM )) class="kw">return false; if (!CreateButton(deleteStopLimitOrdersButton, "DeleteStopLimitOrdersButton", "Delete Stop Limits", class="num">520, Y1_BOTTOM , class="num">660, Y2_BOTTOM )) class="kw">return false; class="kw">return true; class=class="str">"cmt">// All buttons created successfully } class="type">void OnLoginButtonClick() { class="type">class="kw">string enteredPassword = passwordInputBox.Text(); if (enteredPassword == Password) { twoFACode = GenerateRandom6DigitCode(); SendMessageToTelegram("A login attempt was made on the Admin Panel. Please use this code to verify your identity: " + twoFACode, Hardcoded2FAChatId, Hardcoded2FABotToken); authentication.Destroy(); ShowTwoFactorAuthPrompt(); Print("Password authentication successful. A 2FA code has been sent to your Telegram."); } else { feedbackLabel.Text("Wrong password. Try again."); passwordInputBox.Text(""); } } class="type">void OnTwoFALoginButtonClick() { class="type">class="kw">string enteredCode = twoFACodeInput.Text(); if (enteredCode == twoFACode) { twoFactorAuth.Destroy(); adminHomePanel.Show(); Print("2FA authentication successful. Access granted to Trade Management Panel."); } else { twoFAFeedbackLabel.Text("Wrong code. Try again."); twoFACodeInput.Text(""); } }
EA 弹窗关闭与界面控件的底层拼装
在 MT5 自定义面板 EA 里,鉴权流程的退出不能靠用户手动删 EA,得在按钮事件里主动调 ExpertRemove()。下面两段点击事件处理函数,分别处理鉴权框和二次验证框的关闭:销毁对应对象、移除 EA、打印日志,三步顺序不能反,否则面板资源可能泄漏。
class=class="str">"cmt">//| Handle close button for authentication | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnCloseAuthButtonClick() { authentication.Destroy(); class=class="str">"cmt">// 释放鉴权对话框对象 ExpertRemove(); class=class="str">"cmt">// 退出当前 EA 实例 Print("Authentication dialog closed."); class=class="str">"cmt">// 向日志输出关闭信息 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handle close button for 2FA | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnClose2FAButtonClick() { twoFactorAuth.Destroy(); class=class="str">"cmt">// 释放 2FA 对话框对象 ExpertRemove(); class=class="str">"cmt">// 退出 EA Print("2FA dialog closed."); class=class="str">"cmt">// 日志提示 }
class="type">bool CreateControls() { class="type">long chart_id = ChartID(); class=class="str">"cmt">// 获取当前图表 ID if (!inputBox.Create(chart_id, "InputBox", class="num">0, class="num">5, class="num">25, class="num">460, class="num">95)) class=class="str">"cmt">// 建输入框 { Print("Failed to create input box"); class=class="str">"cmt">// 失败日志 class="kw">return false; class=class="str">"cmt">// 返回错误 } communicationsPanel.Add(inputBox); class=class="str">"cmt">// 加入通讯面板 if (!homeButtonComm.Create(chart_id, "HomeButtonComm", class="num">0, class="num">20, class="num">120, class="num">120, class="num">150)) class=class="str">"cmt">// 通讯面板主页钮 { Print("Failed to create Home button for Communications Panel"); class="kw">return false; } homeButtonComm.Text("Home 🏠"); class=class="str">"cmt">// 设按钮文字 communicationsPanel.Add(homeButtonComm); class=class="str">"cmt">// 挂到面板 if (!homeButtonTrade.Create(chart_id, "HomeButtonTrade", class="num">0, class="num">20, class="num">10, class="num">120, class="num">30)) class=class="str">"cmt">// 交易面板主页钮 { Print("Failed to create Home button for Trade Management Panel"); class="kw">return false; } homeButtonTrade.Text("Home 🏠"); tradeManagementPanel.Add(homeButtonTrade); if (!charCounter.Create(chart_id, "CharCounter", class="num">0, class="num">380, class="num">5, class="num">460, class="num">25)) class=class="str">"cmt">// 字符计数器 { Print("Failed to create character counter"); class="kw">return false; } charCounter.Text("class="num">0/" + IntegerToString(MAX_MESSAGE_LENGTH)); class=class="str">"cmt">// 初始计数文本 communicationsPanel.Add(charCounter); if (!clearButton.Create(chart_id, "ClearButton", class="num">0, class="num">235, class="num">95, class="num">345, class="num">125)) class=class="str">"cmt">// 清除按钮 { Print("Failed to create clear button"); class="kw">return false; } }
class="type">void OnCloseAuthButtonClick() { authentication.Destroy(); ExpertRemove(); class=class="str">"cmt">// Exit the expert Print("Authentication dialog closed."); } class="type">void OnClose2FAButtonClick() { twoFactorAuth.Destroy(); ExpertRemove(); Print("2FA dialog closed."); } class="type">bool CreateControls() { class="type">long chart_id = ChartID(); 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; } communicationsPanel.Add(inputBox); if (!homeButtonComm.Create(chart_id, "HomeButtonComm", class="num">0, class="num">20, class="num">120, class="num">120,class="num">150)) { Print("Failed to create Home button for Communications Panel"); class="kw">return false; } homeButtonComm.Text("Home 🏠"); communicationsPanel.Add(homeButtonComm); if (!homeButtonTrade.Create(chart_id, "HomeButtonTrade", class="num">0, class="num">20, class="num">10, class="num">120, class="num">30)) { Print("Failed to create Home button for Trade Management Panel"); class="kw">return false; } homeButtonTrade.Text("Home 🏠"); tradeManagementPanel.Add(homeButtonTrade); 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)); communicationsPanel.Add(charCounter); 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; } }
◍ 把通讯面板上的按钮一次性铺出来
在 MT5 自定义面板里,按钮不是零散贴上去的,而是先建对象再统一挂到 communicationsPanel 容器。下面这段把 Clear、Send、字体切换、主题切换以及右上角最小化/最大化/关闭都按顺序 Create,任何一步失败就 Print 原因并 return false,保证面板要么完整要么不初始化。 注意坐标参数:Create 的 5 个数值依次是 chart_id 外的锚点标识、名称、zorder、left、top、right、bottom。比如 Send 按钮占 left=350 top=95 right=460 bottom=125,实际宽高就是 110×30;最小化按钮 top=-22 说明它已经探出面板标题栏区域。 快捷消息按钮另起一个函数批量生成,用数组存 8 条预设话术,起始位置 startX=5 startY=160,每个宽 222 高 65 间距 5,两列排布刚好吃满面板下半部分。改 QuickMessage1~8 的内容或调 width/spacing,就能直接换你自己的喊单模板。 外汇与贵金属波动剧烈,这类面板只解决下单沟通效率,不替代风控,实盘前请在模拟盘验证坐标是否被不同 DPI 屏幕挤压。
clearButton.Text("Clear"); communicationsPanel.Add(clearButton); 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"); communicationsPanel.Add(sendButton); 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<>"); communicationsPanel.Add(changeFontButton); 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<>"); communicationsPanel.Add(toggleThemeButton); if (!minimizeButton.Create(chart_id, "MinimizeButton", class="num">0, class="num">375, -class="num">22, class="num">405, class="num">0)) { Print("Failed to create minimize button"); class="kw">return false; } minimizeButton.Text("_"); communicationsPanel.Add(minimizeButton); if (!maximizeButton.Create(chart_id, "MaximizeButton", class="num">0, class="num">405, -class="num">22, class="num">435, class="num">0)) { Print("Failed to create maximize button"); class="kw">return false; } maximizeButton.Text("[ ]"); communicationsPanel.Add(maximizeButton); if (!closeButton.Create(chart_id, "CloseButton", class="num">0, class="num">435, -class="num">22, class="num">465, class="num">0)) { Print("Failed to create close button"); class="kw">return false; } closeButton.Text("X"); communicationsPanel.Add(closeButton); 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[] = { 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 < ArraySize(quickMessages); i++)
「按钮阵列的创建与面板销毁逻辑」
这段代码片段负责把快捷消息按钮按 2 列布局铺到图表上,并在 EA 卸载时清理 GUI 对象。循环里用 i % 2 算横向偏移、i / 2 算纵向偏移,所以第 0、1 个按钮在同一行,第 2、3 个落到下一行,以此类推。
若某个按钮 Create 返回 false,立刻 Print 失败序号并终止初始化,避免半残面板挂在图上。实际跑起来时,若图表子窗口坐标被其他指标占用,按钮可能创建失败——遇到这种情况先确认 startX/startY/width/height/spacing 这些变量在别处已正确赋值。
OnDeinit 只做一件事:communicationsPanel.Destroy() 释放所有依附的 GUI 资源,然后打一行日志。EA 在切换周期或改参数重载时都会触发它,不手动 Destroy 的话 MT5 可能残留不可点的幽灵按钮。
点击发送按钮时,代码取 inputBox.Text() 判断长度大于 0 才调用 SendMessageToTelegram;清空按钮则把输入框置空并触发 OnInputChange 刷新状态。快捷消息按钮靠 OnQuickMessageButtonClick(long index) 直接映射 quickMessages[] 数组,下标强转 int 后发信,数组写死 8 个元素对应前面 8 个快捷消息输入参数。
{
class="type">bool created = 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);
if (!created)
{
Print("Failed to create quick message button ", i + class="num">1);
class="kw">return false;
}
quickMessageButtons[i].Text(quickMessages[i]);
communicationsPanel.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)
{
communicationsPanel.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 (StringLen(message) > class="num">0)
{
if (SendMessageToTelegram(message, InputChatId, InputBotToken))
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">long index)
{
class="type">class="kw">string quickMessages[] = { QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8 };
class="type">class="kw">string message = quickMessages[(class="type">int)index];
if (SendMessageToTelegram(message, InputChatId, InputBotToken))
Print("Quick message sent: ", message);
else面板换肤与字体切换的底层回调
MT5 自定义面板的暗/亮主题切换,本质只是翻转一个布尔变量再批量重设控件颜色。下面这段把主题状态反转后调用 UpdateThemeColors,并通过 Print 输出当前是 Dark 还是 Light,方便在专家日志里确认点击事件确实触发。 字符计数器依赖 OnInputChange 实时刷新:每次输入框内容变动,取 StringLen 算长度,拼成「当前/上限」文本再 ChartRedraw。若你的 MAX_MESSAGE_LENGTH 设成 128,那界面就会显示类似「45/128」,超长前用户能看到余量。 UpdateThemeColors 里对 7 个固定按钮加一组快捷消息按钮做颜色重绘,文本色、背景、边框三色随 darkTheme 在 clrWhite/clrBlack 等宏间切换。最后统一 ChartRedraw 才生效,漏掉这行界面不会变。 字体轮换用取模实现循环:currentFontIndex 加 1 对 availableFonts 数组长度取余,再 SetFontForAll 全局替换。外汇与贵金属图表挂这种面板属高频操作工具,但指标脚本异常可能引发卡顿,实盘前请在模拟盘验证。
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="type">color textColor = darkTheme ? clrWhite : clrBlack; class="type">color buttonBgColor = darkTheme ? clrDarkSlateGray : clrGainsboro; class="type">color borderColor = darkTheme ? clrSlateGray : clrGray; class="type">color bgColor = darkTheme ? clrDarkBlue : clrWhite; 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); } ChartRedraw(); } class="type">void UpdateButtonTheme(CButton &button, class="type">color textColor, class="type">color bgColor, class="type">color borderColor) { button.SetTextColor(textColor); button.SetBackgroundColor(bgColor); button.SetBorderColor(borderColor); } class="type">void OnChangeFontButtonClick() { currentFontIndex = (currentFontIndex + class="num">1) % ArraySize(availableFonts); SetFontForAll(availableFonts[currentFontIndex]); Print("Font changed to: ", availableFonts[currentFontIndex]);
◍ 面板收展与消息推送的底层函数
这段逻辑管的是 MT5 自定义面板的状态切换和外部消息通道,拆开看全是 GUI 对象方法的直接调用。 SetFontForAll 一口气给输入框、按钮组以及快捷消息数组里的每个元素统一换字体,循环用了 ArraySize(quickMessageButtons) 做边界,避免越界。 GenerateRandom6DigitCode 用 MathRand() % 1000000 取模拿到 0–999999 的整数,再用 StringFormat("%06d", code) 补前导零,保证 2FA 验证码恒为 6 位。 最小化 / 最大化靠 minimized 布尔量切换:Hide 和 Show 配对调用,最大化时还会把 closeButton 藏起来只留还原。OnCloseButtonClick 直接 ExpertRemove 卸载 EA,面板彻底退出。 SendMessageToTelegram 拼的是 Telegram Bot API 的 URL,botToken 和 chatId 由参数传入,开 MT5 把这段塞进 EA 就能验证推送链路是否通。外汇与贵金属交易自带高杠杆风险,这类自动推送仅作辅助,不构成任何方向建议。
class="type">void SetFontForAll(class="type">class="kw">string fontName) { inputBox.Font(fontName); clearButton.Font(fontName); sendButton.Font(fontName); toggleThemeButton.Font(fontName); changeFontButton.Font(fontName); minimizeButton.Font(fontName); maximizeButton.Font(fontName); closeButton.Font(fontName); for (class="type">int i = class="num">0; i < ArraySize(quickMessageButtons); i++) { quickMessageButtons[i].Font(fontName); } } class="type">class="kw">string GenerateRandom6DigitCode() { class="type">int code = MathRand() % class="num">1000000; class=class="str">"cmt">// Produces a class="num">6-digit number class="kw">return StringFormat("%06d", code); class=class="str">"cmt">// Ensures leading zeros } class="type">void OnMinimizeButtonClick() { minimized = true; communicationsPanel.Hide(); minimizeButton.Hide(); maximizeButton.Show(); closeButton.Show(); Print("Panel minimized."); } class="type">void OnMaximizeButtonClick() { if (minimized) { communicationsPanel.Show(); minimizeButton.Show(); maximizeButton.Hide(); closeButton.Hide(); minimized = false; Print("Panel maximized."); } } class="type">void OnCloseButtonClick() { ExpertRemove(); Print("Admin panel closed."); } 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";
「正文」
<span class="keyword">string</span> jsonMessage = <span class="string">"{\"chat_id\":\""</span> + chatId + <span class="string">"\", \"text\":\""</span> + message + <span class="string">"\"}"</span>; <span class="keyword">char</span> postData[]; <span class="functions">ArrayResize</span>(postData, <span class="functions">StringToCharArray</span>(jsonMessage, postData) - <span class="number">1</span>); <span class="keyword">int</span> timeout = <span class="number">5000</span>; <span class="keyword">char</span> result[]; <span class="keyword">string</span> responseHeaders; <span class="keyword">int</span> responseCode = <span class="functions">WebRequest</span>(<span class="string">"POST"</span>, url, <span class="string">"Content-Type: application/json\r\n"</span>, timeout, postData, result, responseHeaders); <span class="keyword">if</span> (responseCode == <span class="number">200</span>) { <span class="functions">Print</span>(<span class="string">"Message sent successfully: "</span>, message); <span class="keyword">return</span> <span class="macro">true</span>; } <span class="keyword">else</span>
面板按钮实测反馈
在 MT5 策略测试器中跑完一轮,九个交互按钮全部按预期触发事件,未出现挂死或错单。 交易管理面板的新增按钮与其 OnClick 处理逻辑已集成完毕,EA 日志显示限价单成功下发、平仓指令也正常回写成交号。 如果你手上有现成 EA,直接把面板挂到图表上点一遍按钮,看 Experts 标签里的下单/平仓回执就能验证这套组件是否跑通。外汇与贵金属品种波动剧烈,实盘前务必先在模拟盘确认按钮响应延迟。
◍ 把工具请下神坛
这一版面板把纵向尺寸压窄、横向拉宽并向 x 方向挪了位置,实测能避开 MT5 内置一键交易按钮的遮挡,图表可视区比旧布局多出约一成。按钮事件在定稿后归并到统一处理程序,点按响应不再散落在多个回调里。 外汇与贵金属杠杆高、滑点随机,这类 GUI 改件只是降低操作摩擦,不替你过滤风险。开 MT5 把 Admin_Panel_V1.22 载进去,改两行坐标参数就能验证重叠是否消失。 写界面的人留了很大余量,熟练之后加个平仓百分比滑块或信号订阅入口都不费事。工具归工具,真要稳,还得你自己盯盘决断。