MQL5 初学者: 图形对象的防破坏保护·进阶篇
🛡️

MQL5 初学者: 图形对象的防破坏保护·进阶篇

(2/3)·人工误删或第三方程序清场后,你的控制面板如何不变成无主幽灵对象

偏理论进阶 第 2/3 篇
很多新手在图表上跑指标,最怕别人或别的 EA 一键 ObjectsDeleteAll 把面板抹掉,程序从此失去控制。更隐蔽的是对象被改名后原地残留,你以为删了其实还在。本文聚焦未授权修改与删除时的回应框架,不教你写强硬锁死代码。

图表对象的自我分离与自愈框架

在 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 挂上,点几下「是/否」再手动删面板,看它是静默退还是自愈,比读文档直观。

MQL5 / C++
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 改成不同值,能看到右列和左列行数错位,验证布局算法。

MQL5 / C++
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 看面板反应最直观。

MQL5 / C++
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 看差异。外汇贵金属图表对象管理涉及自动删改,误操作可能动到人工画线,属高风险动作,建议先在模拟盘图表试。

MQL5 / C++
  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--)

「正文」

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; obj_name=<span class="functions">ObjectName</span>(chart_ID,i,sub_window,type); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; create_time_x=(<span class="keyword">datetime</span>)<span class="functions">ObjectGetInteger</span>(chart_ID,obj_name, &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="macro">OBJPROP_CREATETIME</span>); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//---</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(create_time_x==create_time) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {ObDelete(chart_ID,obj_name);} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp; } &nbsp;&nbsp; <span class="keyword">return</span>; &nbsp;&nbsp;} <span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">bool</span> ObDelete(<span class="keyword">long</span> chart_ID,<span class="keyword">string</span> name) &nbsp;&nbsp;{ &nbsp;&nbsp; <span class="keyword">if</span>(<span class="functions">ObjectFind</span>(chart_ID,name)&gt;-<span class="number">1</span>) &nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ResetLastError</span>(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span c

◍ 对象删除事件的防循环恢复逻辑

在 EA 初始化收尾阶段,先把 flagAntivandalflagResetEventDelete 置为 -1,随后调用 PanelCreate()RevisionCreateTime()。若后者返回 false,程序直接以 REASON_INITFAILED 退出,避免带着错误时间数组运行。 当图表抛出 CHARTEVENT_OBJECT_DELETECHARTEVENT_OBJECT_CHANGE 时,若 flagAntivandal==-1 直接 return,相当于初始化未完成前不响应任何对象变动。若 flagResetEventDelete>0,则用 StringFind(sparam,prefixObj) 取前缀位置,命中 0 才将计数器减 1 并 return,这一步专门切断“删除→恢复→再触发删除事件”的队列循环。 正常防护态下(flagAntivandal==0 && flagResetEventDelete==0),同样用 StringFind 比对前缀,返回 0 即视为受保护对象,提取 sparamchartObject 并准备恢复。外汇与贵金属图表上手动删线频繁,这套前缀匹配 + 事件计数器的写法能显著降低误恢复概率。

MQL5 / C++
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">/*检查对象是否"被保护",
         如果是, 就进行恢复工作:*/
把对象存活检查交给小布
小布盯盘的 AIGC 已内置对象状态巡检,打开对应品种页就能看到面板对象是否失联或被改名,不必自己写事件轮询。把重复劳动交给小布,你专注决策。

常见问题

可通过事件 ID 与对象前缀比对来区分,人工修改通常触发属性事件,第三方程序删除则对象枚举消失,文中框架段有具体判定路径。
倾向在多人共用图表环境用自我分离避免幽灵对象,单一策略环境可用自我恢复保住面板,概率上后者维护成本更高。
可以,小布盯盘的品种页内置对象存活诊断,能提示对象失联或被重命名,省去你手写 ChartEvent 轮询。
终端首要解决交易者问题,武断干预其他程序或用户操作不可接受,文中用清洁工类比说明了激进回应的不当。
在创建对象时固化专属前缀并在事件里校验,未授权删除触发恢复或分离,外汇贵金属图表高风险环境更需注意。