MQL5 初学者: 图形对象的防破坏保护·进阶篇
(2/3)·人工误删或第三方程序清场后,你的控制面板如何不变成无主幽灵对象
图表对象的自我分离与自愈框架
在 MT5 里跑指标时,最烦的是面板对象被手动改了属性或删掉,程序直接崩。作者用一套测试指标 test_count_click_0 作底,派生出两种框架:一种是程序检测到对象被改/删就自己退出图表(自我分离),另一种是自动把对象重建回来(自我恢复)。两个完整版代码分别是 test_count_click_1 和 test_count_click_2,都带 Print() 输出,能在「专家」标签页看清处理顺序。 自我分离框架靠一个 flagAntivandal 信号灯控制提醒:初始化设 -1 表示静默,面板建完再设 0 启用。OnChartEvent() 里先用 StringFind() 拿前缀过滤,不匹配直接退;匹配上再全名比对,命中就把标志改回 -1 防循环。若对象被改名成「无主」,就按创建时间用 ObDelCreateTimeExcept0() 清掉——这个函数特意跳过创建时间等于 0(1970.01.01 00:00)的对象,避免终端重启后误删。 自我恢复更复杂。test_count_click_2 多了一个辅助标志 flagResetEventDelete,主标志管「提醒」、辅助标志管「重删事件计数」。删对象时辅助标志先置为「受保护对象数减1」,随后 ObDeletePrefixFlag() 按前缀全清,再重绘、重建面板。关键点:恢复函数不按对象类型筛选,因为破坏可能针对任意类型,类型判断反而漏恢复。 外汇/贵金属图表上对象被外力改删是高频风险,这两套框架直接另存附件改前缀就能测。开 MT5 把 test_count_click_1 挂上,点几下「是/否」再手动删面板,看它是静默退还是自愈,比读文档直观。
class="macro">#define NAME_INDICATOR "count_click_1: " class="type">int flagAntivandal; class="type">class="kw">datetime timeCreateObj[QUANT_OBJ]; class="type">class="kw">string textDelInd[class="num">2]; class="type">void LanguageTerminal() { class="type">class="kw">string language_t=NULL; language_t=TerminalInfoString(TERMINAL_LANGUAGE); class=class="str">"cmt">//--- if(language_t=="Russian") {
「面板双语标签与对象创建的细节」
这段逻辑先按语言分支给统计栏填词:俄语环境下 textObj[3] 到 [5] 分别是「Да / Нет / Всего」,英语环境则换成「Yes / No / All」,同时把指标删除成败的提示串用 StringConcatenate 拼好,prefixObj 作为指标前缀嵌进错误信息里。 PanelCreate 负责把点击计数和注释栏画到图表上。它用 ObjectFind 判断对象是否存在,不存在才调用 EditCreate,坐标按 Y_DISTANCE+(HEIGHT)*i 向下排,字体固定 Arial、对齐方式分 ALIGN_RIGHT 与 ALIGN_LEFT 两种。 循环上界有个容易忽略的点:计数栏用 NUMBER_ALL 倒序建,注释栏则从 QUANT_OBJ-1 跑到 correct(=NUMBER_ALL+1),两者偏移量靠 (i-correct) 校正。开 MT5 把 NUMBER_ALL 和 QUANT_OBJ 改成不同值,能看到右列和左列行数错位,验证布局算法。
textObj[class="num">3]="Да: "; textObj[class="num">4]="Нет: "; textObj[class="num">5]="Всего: "; class=class="str">"cmt">//--- StringConcatenate(textDelInd[class="num">0],"Не удалось удалиться индикатору: \"", prefixObj,"\". Код ошибки: "); StringConcatenate(textDelInd[class="num">1],"Удалился с графика индикатор: \"", prefixObj,"\"."); } else { textObj[class="num">3]="Yes: "; textObj[class="num">4]="No: "; textObj[class="num">5]="All: "; class=class="str">"cmt">//--- StringConcatenate(textDelInd[class="num">0],"删除指标失败: \"", prefixObj,"\". 错误编号: "); StringConcatenate(textDelInd[class="num">1], "从图表指标上撤回: \"", prefixObj,"\"."); } class=class="str">"cmt">//--- class="kw">return; } class="type">void PanelCreate(class="kw">const class="type">long chart_ID=class="num">0,class="kw">const class="type">int sub_window=class="num">0) { for(class="type">int i=NUMBER_ALL;i>=class="num">0;i--) { class=class="str">"cmt">//--- 显示点击数量的栏位: if(ObjectFind(chart_ID,nameObj[i])<class="num">0) { EditCreate(chart_ID,nameObj[i],sub_window,X_DISTANCE+WIDTH, Y_DISTANCE+(HEIGHT)*(i),WIDTH,HEIGHT, textObj[i],"Arial",FONT_SIZE,"\n",ALIGN_RIGHT,true, CORNER_PANEL,clrText[i],CLR_PANEL,CLR_BORDER); class=class="str">"cmt">//--- 获取和存储创建对象的时间: CreateTimeGet(chart_ID,nameObj[i],timeCreateObj[i]); } } class=class="str">"cmt">//--- class="type">int correct=NUMBER_ALL+class="num">1; class=class="str">"cmt">//--- for(class="type">int i=QUANT_OBJ-class="num">1;i>=correct;i--) { class=class="str">"cmt">//--- 注释栏位: if(ObjectFind(chart_ID,nameObj[i])<class="num">0) { EditCreate(chart_ID,nameObj[i],sub_window,X_DISTANCE+(WIDTH*class="num">2), Y_DISTANCE+(HEIGHT)*(i-correct),WIDTH,HEIGHT, textObj[i],"Arial",FONT_SIZE,"\n",ALIGN_LEFT,true,
◍ 对象时间戳与防篡改事件钩子
这段逻辑解决一个实盘里容易忽略的问题:指标画的图形对象被手动删掉或改了属性,面板却还显示旧数据。先把每个对象的创建时间用 CreateTimeGet 抓出来存进数组,后面比对才知道是不是被动过。 CreateTimeGet 内部调 ObjectGetInteger 取 OBJPROP_CREATETIME,失败就打印行号和错误码并返回 false;成功才把 res 写回引用参数 value。RevisionCreateTime 则倒序扫一遍时间数组,遇到 0 就说明对象创建记录缺失,直接报错退出。 初始化里先用 StringConcatenate 拼出带品种和周期的前缀,比如 EURUSD_H1,再设成指标短名并建面板。接着调 RevisionCreateTime 校验,失败则返回 REASON_INITFAILED 触发删除。 防篡改靠 flagAntivandal:置 0 表示启用对象删除/修改事件提醒,置 -1 则 OnChartEvent 里直接 return 忽略。ChartEventObjectDeleteGet 查当前通知状态,没开就 ChartEventObjectDeleteSet(true) 打开。EventSetTimer(5) 起 5 秒定时器,退出前 EventKillTimer 清掉。外汇和贵金属波动剧烈、滑点频发,这类对象失控可能导致信号显示失真,上 MT5 跑一遍改 flagAntivandal 看面板反应最直观。
class="type">bool CreateTimeGet(class="kw">const class="type">long chart_ID, class="kw">const class="type">class="kw">string &name, class="type">class="kw">datetime &value ) { class="type">class="kw">datetime res=class="num">0; if(!ObjectGetInteger(chart_ID,name,OBJPROP_CREATETIME,class="num">0,res)) { Print(LINE_NUMBER,__FUNCTION__,", 错误 = ", GetLastError(),", 名称: ",name); class="kw">return(class="kw">false); } value=res; class="kw">return(true); } class="type">bool RevisionCreateTime(class="type">int quant,class="type">class="kw">datetime &time_create[]) { class="type">class="kw">datetime t=class="num">0; for(class="type">int i=quant-class="num">1;i>=class="num">0;i--) { t=time_create[i]; if(t==class="num">0) { Print(LINE_NUMBER,__FUNCTION__,", 错误创建时间: ", TimeToString(t,TIME_DATE|TIME_SECONDS)); class="kw">return(class="kw">false); } } class=class="str">"cmt">//--- class="kw">return(true); } flagAntivandal=-class="num">1; class=class="str">"cmt">//--- 创建代码中对象名称的前缀和指标的短名称: StringConcatenate(prefixObj,NAME_INDICATOR,Symbol(),"_",EnumToString(Period())); class=class="str">"cmt">//--- 设置指标的短名称: IndicatorSetString(INDICATOR_SHORTNAME,prefixObj); class=class="str">"cmt">//--- 创建面板: PanelCreate(); class=class="str">"cmt">/*修改创建对象时间的数组, 如果出错则程序退出:*/ if(!RevisionCreateTime(QUANT_OBJ,timeCreateObj)) {class="kw">return(REASON_INITFAILED);} class=class="str">"cmt">/*我们设置回应标志 以对应对象的删除和修改事件:*/ flagAntivandal=class="num">0; class=class="str">"cmt">//--- 启用图形对象删除事件的通知 class="type">bool res=true; ChartEventObjectDeleteGet(res); if(res!=true) {ChartEventObjectDeleteSet(true);} EventSetTimer(class="num">5); flagAntivandal=-class="num">1; EventKillTimer(); class=class="str">"cmt">//--- 如果 OnInit() 传来 REASON_INITFAILED, 删除指标. if(reason==REASON_INITFAILED) { ChIndicatorDelete(prefixObj,textDelInd[class="num">0],textDelInd[class="num">1],class="num">0,class="num">0); } else if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE) { if(flagAntivandal==-class="num">1)class=class="str">"cmt">//如果提醒被禁止, 则忽略事件 {class="kw">return;} else if(flagAntivandal==class="num">0)class=class="str">"cmt">//如果提醒被启用, {
按创建时间清理同名图表对象
在 MT5 图表事件里处理对象删除时,先用 StringFind 判断参数是否带指定前缀,命中后倒序比对已登记对象名。名称完全一致就置位 flagAntivandal=-1,阻断重复触发删除的死循环,这是图表脚本防自残的关键一环。 ObDelCreateTimeExcept0 这个函数专门按创建时间清对象,但有个硬门槛:传入的 create_time 若为 0(即 1970.01.01 00:00:00),直接 return。终端重启后手工或程序建的对象创建时间会被清零,这类不能误删。 函数内用 ObjectsTotal 取总数,>0 才进循环。实际跑下来,若图表挂了 30 个 OBJ_EDIT 框且重启过,至少会剩一批时间=0 的框逃过清理,验证时可在 OnChartEvent 里打印 quant_obj 和 create_time 看差异。外汇贵金属图表对象管理涉及自动删改,误操作可能动到人工画线,属高风险动作,建议先在模拟盘图表试。
class=class="str">"cmt">//--- 寻找前缀的匹配: findPrefixObject=StringFind(sparam,prefixObj); if(findPrefixObject==class="num">0)class=class="str">"cmt">//如果前缀匹配 { class="type">class="kw">string chartObject=sparam; findPrefixObject=-class="num">1; class=class="str">"cmt">//--- for(class="type">int i=QUANT_OBJ-class="num">1;i>=class="num">0;i--) { class=class="str">"cmt">//--- 当名称完全匹配: if(StringCompare(chartObject,nameObj[i],true)==class="num">0) { class=class="str">"cmt">//--- 禁止提醒以避免循环删除操作: flagAntivandal=-class="num">1; class=class="str">"cmt">/*删除创建时间等于修改或者删除对象的创建时间 的对象:*/ ObDelCreateTimeExcept0(timeCreateObj[i],class="num">0,class="num">0,OBJ_EDIT); class=class="str">"cmt">//--- 从图表上删除指标: ChIndicatorDelete(prefixObj,textDelInd[class="num">0],textDelInd[class="num">1],class="num">0,class="num">0); class=class="str">"cmt">//--- ChartRedraw(); class="kw">return; } }class=class="str">"cmt">//for(class="type">int i=QUANT_OBJ-class="num">1;i>=class="num">0;i--) class="kw">return; }class=class="str">"cmt">//if(findPrefixObject==class="num">0) class="kw">return; }class=class="str">"cmt">//if(flagAntivandal==class="num">0) class="kw">return; }class=class="str">"cmt">//if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE) class="type">void ObDelCreateTimeExcept0(class="type">class="kw">datetime &create_time,class=class="str">"cmt">// 创建时间 class="type">long chart_ID=class="num">0,class=class="str">"cmt">// 图表id class="type">int sub_window=-class="num">1,class=class="str">"cmt">// 子窗口编号 ENUM_OBJECT type=-class="num">1)class=class="str">"cmt">// -class="num">1 = 全部类型的对象 { class=class="str">"cmt">/*从函数退出, 如果对象时间 = class="num">0(class="num">1970.01.class="num">01 class="num">00:class="num">00:class="num">00), 所以不需要从图表上删除人工或者程序创建的对象, 它们的创建时间 在终端重新启动的时候变成0了*/ if(create_time==class="num">0){class="kw">return;} class="type">int quant_obj=class="num">0; quant_obj=ObjectsTotal(chart_ID,sub_window,type); class=class="str">"cmt">//--- if(quant_obj>class="num">0) { class="type">class="kw">datetime create_time_x=class="num">0; class="type">class="kw">string obj_name=NULL; class=class="str">"cmt">//--- for(class="type">int i=quant_obj-class="num">1;i>=class="num">0;i--)
「正文」
{ obj_name=<span class="functions">ObjectName</span>(chart_ID,i,sub_window,type); create_time_x=(<span class="keyword">datetime</span>)<span class="functions">ObjectGetInteger</span>(chart_ID,obj_name, <span class="macro">OBJPROP_CREATETIME</span>); <span class="comment">//---</span> <span class="keyword">if</span>(create_time_x==create_time) {ObDelete(chart_ID,obj_name);} } } <span class="keyword">return</span>; } <span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">bool</span> ObDelete(<span class="keyword">long</span> chart_ID,<span class="keyword">string</span> name) { <span class="keyword">if</span>(<span class="functions">ObjectFind</span>(chart_ID,name)>-<span class="number">1</span>) { <span class="functions">ResetLastError</span>(); <span c
◍ 对象删除事件的防循环恢复逻辑
在 EA 初始化收尾阶段,先把 flagAntivandal 与 flagResetEventDelete 置为 -1,随后调用 PanelCreate() 与 RevisionCreateTime()。若后者返回 false,程序直接以 REASON_INITFAILED 退出,避免带着错误时间数组运行。
当图表抛出 CHARTEVENT_OBJECT_DELETE 或 CHARTEVENT_OBJECT_CHANGE 时,若 flagAntivandal==-1 直接 return,相当于初始化未完成前不响应任何对象变动。若 flagResetEventDelete>0,则用 StringFind(sparam,prefixObj) 取前缀位置,命中 0 才将计数器减 1 并 return,这一步专门切断“删除→恢复→再触发删除事件”的队列循环。
正常防护态下(flagAntivandal==0 && flagResetEventDelete==0),同样用 StringFind 比对前缀,返回 0 即视为受保护对象,提取 sparam 为 chartObject 并准备恢复。外汇与贵金属图表上手动删线频繁,这套前缀匹配 + 事件计数器的写法能显著降低误恢复概率。
flagAntivandal = flagResetEventDelete = -class="num">1; class=class="str">"cmt">//--- 创建面板: PanelCreate(); class=class="str">"cmt">/*修改创建对象时间的数组, 如果出错则程序退出:*/ if(!RevisionCreateTime(QUANT_OBJ,timeCreateObj)) {class="kw">return(REASON_INITFAILED);} class=class="str">"cmt">/*设置反恶意破坏标志, 位置在 对删除或者修改对象进行回应的地方:*/ flagAntivandal=flagResetEventDelete=class="num">0; else if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE) { if(flagAntivandal==-class="num">1){class="kw">return;} else if(flagResetEventDelete>class="num">0) { findPrefixObject=StringFind(sparam,prefixObj); if(findPrefixObject==class="num">0) { findPrefixObject=-class="num">1; class=class="str">"cmt">/*重置事件 计数器标志以避免 在删除对象时事件形成队列造成循环 以至于恢复工作错误:*/ flagResetEventDelete=flagResetEventDelete-class="num">1; class=class="str">"cmt">//--- class="kw">return; } class="kw">return; } else if(flagAntivandal==class="num">0 && flagResetEventDelete==class="num">0) { class=class="str">"cmt">//--- 根据前缀检查匹配: findPrefixObject=StringFind(sparam,prefixObj); if(findPrefixObject==class="num">0)class=class="str">"cmt">//如果前缀匹配 { class="type">class="kw">string chartObject=sparam;class=class="str">"cmt">//事件中的对象名称 findPrefixObject=-class="num">1; class=class="str">"cmt">/*检查对象是否"被保护", 如果是, 就进行恢复工作:*/