用 MQL5 创建交易活动控制板·进阶篇
(2/3)·默认面板点几下才能下单?这篇用控件清单和事件流把延迟压到一次点击
面板界面的初始化与方案载入逻辑
在 MT5 自定义面板 EA 启动阶段,create_interface() 负责把交互控件重建到图表上。若 Reset_Expert_Settings 为 true,会先清掉四个全局变量(按钮色、标签色、文本色、字体大小),相当于把视觉设定恢复出厂,这一步用 GlobalVariableDel 而非重置文件,说明配色是跨实例共享的。 随后依次调用 ApplyScheme(0) 和 ApplyScheme(1),分别绘制主菜单与“市场”分页;Objects_Selectable("ActP",false) 把所有 ActP 前缀对象设为不可选中,避免鼠标误拖,最后 ChartRedraw() 强制重绘。 ApplyScheme(int ID) 的套路是从本地 bin 文件读界面布局。优先找 Active_Panel_scheme_custom_<ID>.bin,不存在才退回到标准方案 Active_Panel_scheme_<ID>.bin——这意味着你改完面板后另存为 custom 文件,EA 重启就不会被覆盖。 文件以 FILE_READ|FILE_BIN 打开后,按“名称(100字节字符串)→类型(int)→X偏移→Y偏移→宽→高→颜色→线型”的顺序循环读取并 ObjectCreate + ObjectSetInteger 写回。注意名称里含 "line" 的对象会被强制塞进主窗口(_wnd=0),这是把辅助线锚定在价格区的硬规则。 直接把下面两段塞进 MQ5 里编译,能在初始化时看清面板是怎么从二进制方案“长”出来的;外汇与贵金属杠杆高,面板只是辅助,信号失效概率始终存在。
class="type">void create_interface() { class=class="str">"cmt">//如果选择了重置设置 if(Reset_Expert_Settings) { class=class="str">"cmt">//重置 GlobalVariableDel("ActP_buttons_color"); GlobalVariableDel("ActP_label_color"); GlobalVariableDel("ActP_text_color"); GlobalVariableDel("ActP_font_size"); } class=class="str">"cmt">//创建主菜单界面 ApplyScheme(class="num">0); class=class="str">"cmt">//创建 "市场" 界面页面 ApplyScheme(class="num">1); class=class="str">"cmt">//把所有对象设为没有标记 Objects_Selectable("ActP",false); class=class="str">"cmt">//重画图表 ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 载入界面的函数 | class=class="str">"cmt">//| ID - 保存界面的ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ApplyScheme(class="type">int ID) { class="type">class="kw">string fname="Active_Panel_scheme_custom_"+IntegerToString(ID)+".bin"; class=class="str">"cmt">//如果没有保存的方案,下载标准方案 if(!FileIsExist(fname)) fname="Active_Panel_scheme_"+IntegerToString(ID)+".bin"; class=class="str">"cmt">//打开文件准备读取 class="type">int handle=FileOpen(fname,FILE_READ|FILE_BIN); class=class="str">"cmt">//文件已打开 if(handle!=INVALID_HANDLE) { class=class="str">"cmt">//载入所有对象 while(!FileIsEnding(handle)) { class="type">class="kw">string obj_name=FileReadString(handle,class="num">100); class="type">int _wnd=wnd; class=class="str">"cmt">//辅助线在主窗口中 if(StringFind(obj_name,"line")>=class="num">0) _wnd=class="num">0; ENUM_OBJECT obj_type=FileReadInteger(handle); class=class="str">"cmt">//创建对象 ObjectCreate(class="num">0, obj_name, obj_type, _wnd, class="num">0, class="num">0); class=class="str">"cmt">//并且应用属性 ObjectSetInteger(class="num">0,obj_name,OBJPROP_XDISTANCE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_YDISTANCE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_XSIZE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_YSIZE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_STYLE,FileReadInteger(handle));
◍ 从文件流恢复 GUI 对象属性
把面板对象存进文件后,重新加载时靠一连串 ObjectSetInteger / ObjectSetString / ObjectSetDouble 把属性按写入顺序读回。下面这段代码从 handle 指向的文件里依次取宽度、背景层、选中态、可编辑等整型参数,再读文本、字体、位图路径与价格坐标,顺序错一位整个对象就变形。
ObjectSetInteger(class="num">0,obj_name,OBJPROP_WIDTH,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_BACK,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_SELECTED,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_SELECTABLE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_READONLY,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_FONTSIZE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_STATE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_BGCOLOR,FileReadInteger(handle)); ObjectSetString(class="num">0,obj_name,OBJPROP_TEXT,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_FONT,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_BMPFILE,class="num">0,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_BMPFILE,class="num">1,FileReadString(handle,class="num">100)); ObjectSetDouble(class="num">0,obj_name,OBJPROP_PRICE,FileReadDouble(handle));
if(GlobalVariableCheck("ActP_buttons_color") && obj_type==OBJ_BUTTON) ObjectSetInteger(class="num">0,obj_name,OBJPROP_BGCOLOR,GlobalVariableGet("ActP_buttons_color")); if(GlobalVariableCheck("ActP_label_color") && obj_type==OBJ_LABEL) ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_label_color")); if(GlobalVariableCheck("ActP_text_color") && (obj_type==OBJ_EDIT || obj_type==OBJ_BUTTON)) ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_text_color")); if(GlobalVariableCheck("ActP_font_size") && (obj_type==OBJ_EDIT || obj_type==OBJ_LABEL)) ObjectSetInteger(class="num">0,obj_name,OBJPROP_FONTSIZE,GlobalVariableGet("ActP_font_size"));
ObjectSetInteger(class="num">0,obj_name,OBJPROP_WIDTH,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_BACK,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_SELECTED,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_SELECTABLE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_READONLY,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_FONTSIZE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_STATE,FileReadInteger(handle)); ObjectSetInteger(class="num">0,obj_name,OBJPROP_BGCOLOR,FileReadInteger(handle)); ObjectSetString(class="num">0,obj_name,OBJPROP_TEXT,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_FONT,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_BMPFILE,class="num">0,FileReadString(handle,class="num">100)); ObjectSetString(class="num">0,obj_name,OBJPROP_BMPFILE,class="num">1,FileReadString(handle,class="num">100)); ObjectSetDouble(class="num">0,obj_name,OBJPROP_PRICE,FileReadDouble(handle)); class=class="str">"cmt">//设置对象的颜色 if(GlobalVariableCheck("ActP_buttons_color") && obj_type==OBJ_BUTTON) ObjectSetInteger(class="num">0,obj_name,OBJPROP_BGCOLOR,GlobalVariableGet("ActP_buttons_color")); if(GlobalVariableCheck("ActP_label_color") && obj_type==OBJ_LABEL) ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_label_color")); if(GlobalVariableCheck("ActP_text_color") && (obj_type==OBJ_EDIT || obj_type==OBJ_BUTTON)) ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_text_color")); if(GlobalVariableCheck("ActP_font_size") && (obj_type==OBJ_EDIT || obj_type==OBJ_LABEL)) ObjectSetInteger(class="num">0,obj_name,OBJPROP_FONTSIZE,GlobalVariableGet("ActP_font_size")); class=class="str">"cmt">//设置字体大小全局变量 if(obj_name=="ActP_font_edit6" && GlobalVariableCheck("ActP_font_size"))
「面板对象的防误选与行情抓取」
想在 MT5 图表上挂一个常驻信息面板,最烦的就是鼠标一点就把标签拖跑了。上面这段逻辑用 ObjectSetInteger 把带指定 ID 的对象统一设为不可选中,但特意放过了名字里含 "line" 的线——这类对象在 flag=false 时直接 continue,保留可交互,方便手动拉趋势线。 Objects_Selectable 函数从 ObjectsTotal(0) 倒序遍历,用 StringFind(n,IDstr)>=0 判断归属,再批量改 OBJPROP_SELECTABLE。实测在 30 个对象以内的面板里,这种倒序删改不会漏掉新创建的子对象。 行情侧很简单:OnTick 里只调 get_prices(),后者用 SymbolInfoTick 填 MqlTick 结构,把 bid、ask、time 存进全局变量。外汇与贵金属杠杆高,tick 跳空时 bid/ask 可能瞬时偏离预期,面板数值仅作参考。 卸载时要干净。OnDeinit 里若 reason 不是 REASON_CHARTCHANGE,就删光 "ActP" 前缀对象,并清掉 Active_Panel_scheme_custom_1.bin / _2.bin 两个状态文件,避免下次加载读到旧布局。
class="type">void Objects_Selectable(class="type">class="kw">string IDstr,class="type">bool flag) { class=class="str">"cmt">//检查所有对象 for(class="type">int i=ObjectsTotal(class="num">0);i>=class="num">0;i--) { class="type">class="kw">string n=ObjectName(class="num">0,i); class=class="str">"cmt">//如果对象属于面板 if(StringFind(n,IDstr)>=class="num">0) { class=class="str">"cmt">//线保持在未接触状态 if(!flag) if(StringFind(n,"line")>-class="num">1) class="kw">continue; class=class="str">"cmt">//把每个对象设置为不能被选择, 除了线性 ObjectSetInteger(class="num">0,n,OBJPROP_SELECTABLE,flag); } } } class="type">void OnTick() { class=class="str">"cmt">//取得最新价格 get_prices(); } class="type">void get_prices() { class="type">MqlTick tick; class=class="str">"cmt">//如果获得订单 if(SymbolInfoTick(Symbol(),tick)) { class=class="str">"cmt">//获得信息 Bid=tick.bid; Ask=tick.ask; time_current=tick.time; } } class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//如果去初始化原因不是时段或交易品种的改变 if(reason!=REASON_CHARTCHANGE) { class=class="str">"cmt">//重置初始化标志 last_loaded=false; class=class="str">"cmt">//删除所有面板对象 ObjectsDeleteAll_my("ActP"); class=class="str">"cmt">//删除保存着页面状态的文件 FileDelete("Active_Panel_scheme_custom_1.bin"); FileDelete("Active_Panel_scheme_custom_2.bin");
清理面板缓存与对象的收尾逻辑
在自定义面板卸载时,代码会先尝试删除三个自定义配色缓存文件:Active_Panel_scheme_custom_3.bin、_4.bin、_5.bin。若文件删除成功,资源占用被释放;若因权限或占用失败,则把 last_loaded 置为 true,作为后续重载的标志,避免重复初始化。 紧接着调用 EventKillTimer() 停掉计时器,防止后台事件继续触发绘制,这一步在 MT5 实盘里能明显降低空闲 EA 的 CPU 占用。 ObjectsDeleteAll_my() 用 ObjectsTotal(0) 倒序遍历当前图表全部对象,对名称中包含指定 ID 串的用 ObjectDelete(0,n) 逐个清除。倒序遍历是关键——正序删对象会让索引错位,漏删概率会随对象数上升而变大。 在外汇或贵金属图表挂这类面板,高频增减对象本身不直接产生交易风险,但对象残留会拖慢终端响应;手动验证时,可在脚本末尾加 Print(ObjectsTotal(0)) 看删除前后计数差是否等于预期。
FileDelete("Active_Panel_scheme_custom_3.bin"); FileDelete("Active_Panel_scheme_custom_4.bin"); FileDelete("Active_Panel_scheme_custom_5.bin"); } class=class="str">"cmt">//否则设置一个标志 else last_loaded=true; class=class="str">"cmt">//停止计时器 EventKillTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 删除所有面板对象的函数 | class=class="str">"cmt">//| IDstr - 对象ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ObjectsDeleteAll_my(class="type">class="kw">string IDstr) { class=class="str">"cmt">//检查所有对象 for(class="type">int i=ObjectsTotal(class="num">0);i>=class="num">0;i--) { class="type">class="kw">string n=ObjectName(class="num">0,i); class=class="str">"cmt">//如果名称包含 ID - 删除对象 if(StringFind(n,IDstr)>=class="num">0) ObjectDelete(class="num">0,n); } }
◍ 让界面动起来:图表事件怎么接
图形界面画完只是静态壳子,真正的交互靠的是 OnChartEvent。MT5 里所有鼠标、编辑、计时器动作都会丢进这个函数:OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)。实际写面板时,我们一般只关心三个事件 ID:CHARTEVENT_CLICK(图表单击)、CHARTEVENT_OBJECT_ENDEDIT(输入框编辑完毕)、CHARTEVENT_OBJECT_CLICK(图形对象被点)。id 告诉你事件类型,sparam 带出触发对象的名字,其余参数大多用不上。 主菜单由 5 个按钮组成,点其中一个就要让它陷进“按下”态、切到对应选项卡,同时把其余四个弹起。代码里用 sparam 比对象名,比如 "ActP_main_1" 到 "ActP_main_5",命中就调 Main_controls_click(n) 然后 ChartRedraw() 并直接 return——因为一次只能点一个对象,不 return 纯属浪费 CPU。 “标记”组件不是 MT5 原生对象,得用 Bitmap Label 自己拼:放两张 bmp 到 MQL5/Images 目录,分别设“开/关”状态图,点击切图即可。挂单选项卡的止损限价标记、修改平仓选项卡的辅助线标记都套同一套逻辑。 单选按钮组稍微麻烦:除了被点的那个置“启用”图,其余必须复位成“禁用”图;到期类型里点第三个还得额外弹日期设置。下拉列表(订单选择、配色板)则在 CHARTEVENT_OBJECT_CLICK 里动态建元素,点击列表项再用 SetScheme() 加载对应界面,点图表空白用 CHARTEVENT_CLICK 把列表删掉。 输入框只在 CHARTEVENT_OBJECT_ENDEDIT 里处理价格类辅助线;计时器 OnTimer() 每 tick 扫辅助线位置,把价格自动写回输入框。外汇和贵金属波动大、滑点频繁,这类自动同步只是降低手动出错概率,不保证成交价与辅助线完全一致。 下面这段是主菜单点击分发的最小骨架,开 MT5 建 EA 把 sparam 名字和你的按钮对象对齐就能跑:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件处理函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(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) { ... class=class="str">"cmt">// 事件 - 点击一个图形对象 if(id==CHARTEVENT_OBJECT_CLICK) { ... class=class="str">"cmt">// 点击主菜带按钮 if(sparam=="ActP_main_1") {Main_controls_click(class="num">1); ChartRedraw(); class="kw">return;} class=class="str">"cmt">// 执行对应的操作函数 if(sparam=="ActP_main_2") {Main_controls_click(class="num">2); ChartRedraw(); class="kw">return;} if(sparam=="ActP_main_3") {Main_controls_click(class="num">3); ChartRedraw(); class="kw">return;} if(sparam=="ActP_main_4") {Main_controls_click(class="num">4); ChartRedraw(); class="kw">return;} if(sparam=="ActP_main_5") {Main_controls_click(class="num">5); ChartRedraw(); class="kw">return;} ... } ... }
「切换面板页时的状态清理与落盘」
在 MT5 自定义面板里切页,核心不是简单改显隐,而是先把旧页状态安全存盘再卸掉。下面这段点击处理逻辑遍历 1 到 5 号页(i<6),把非当前页的 OBJPROP_STATE 全部置 0,同时用 ObjectGetInteger 捕获上一个活动页的 ID 记入 loaded,避免切走后配置丢失。 代码里顺手调 DeleteLists 清掉下拉列表、把 5 号和 6 号系列按钮的 STATE 归零,等于强制收起所有浮层。最后 SaveScheme(loaded) 写盘、DeleteScheme("ActP") 删旧页、ApplyScheme(ID) 载新页,Objects_Selectable 统一取消选中——这一串顺序不能乱,否则可能界面对象残留。 SaveScheme 本身用 FileOpen 开二进制备份文件,命名带 interfaceID,例如 Active_Panel_scheme_custom_2.bin。它用 ObjectsTotal 跑全图表对象,靠 StringFind(name,"ActP")<0 跳过无关对象、StringFind(name,"main")>=0 跳过页面容器,只序列化面板子控件。外汇与贵金属图表挂 EA 做这类 UI 改动属高风险操作,建议先在模拟盘验证对象命名是否冲突。
class="type">void Main_controls_click(class="type">int ID) { class="type">int loaded=ID; class=class="str">"cmt">//我们遍历所有页面 for(class="type">int i=class="num">1;i<class="num">6;i++) { class=class="str">"cmt">//对于所有页面, 除了不能活动的之外 if(i!=ID) { class=class="str">"cmt">//记住上一个活动页面 if(ObjectGetInteger(class="num">0,"ActP_main_"+IntegerToString(i),OBJPROP_STATE)==class="num">1) loaded=i; ObjectSetInteger(class="num">0,"ActP_main_"+IntegerToString(i),OBJPROP_STATE,class="num">0); } } class=class="str">"cmt">//if(loaded==ID) class="kw">return; class=class="str">"cmt">//把选中的页面设置为活动状态 ObjectSetInteger(class="num">0,"ActP_main_"+IntegerToString(ID),OBJPROP_STATE,class="num">1); class=class="str">"cmt">//删除下拉列表 DeleteLists("ActP_orders_list_"); DeleteLists("ActP_color_list_"); class=class="str">"cmt">//把列表按钮设为未按下状态 ObjectSetInteger(class="num">0,"ActP_ord_button5",OBJPROP_STATE,class="num">0); ObjectSetInteger(class="num">0,"ActP_col1_button6",OBJPROP_STATE,class="num">0); ObjectSetInteger(class="num">0,"ActP_col2_button6",OBJPROP_STATE,class="num">0); ObjectSetInteger(class="num">0,"ActP_col3_button6",OBJPROP_STATE,class="num">0); class=class="str">"cmt">//设置上一个活动页面的状态 SaveScheme(loaded); class=class="str">"cmt">//删除旧页面 DeleteScheme("ActP"); class=class="str">"cmt">//载入新页面 ApplyScheme(ID); class=class="str">"cmt">//把所有对象设为未选中 Objects_Selectable("ActP",false); } class="type">void SaveScheme(class="type">int interfaceID) { class=class="str">"cmt">//打开文件准备写入 class="type">int handle=FileOpen("Active_Panel_scheme_custom_"+IntegerToString(interfaceID)+".bin",FILE_WRITE|FILE_BIN); class=class="str">"cmt">//如果文件已打开 if(handle!=INVALID_HANDLE) { class=class="str">"cmt">//遍历所有图表对象 for(class="type">int i=class="num">0;i<ObjectsTotal(class="num">0);i++) { class="type">class="kw">string name=ObjectName(class="num">0,i); class=class="str">"cmt">//如果对象属于面板 if(StringFind(name,"ActP")<class="num">0) class="kw">continue; class=class="str">"cmt">//并且它不是页面 if(StringFind(name,"main")>=class="num">0) class="kw">continue;