MQL5 初学者: 图形对象的防破坏保护·综合运用
🛡️

MQL5 初学者: 图形对象的防破坏保护·综合运用

(3/3)·当别人或第三方脚本误删你的控制面板对象,程序如何不失控且不留无主残影

案例拆解新手友好 第 3/3 篇
很多人以为图表上的面板对象删了就删了,重拖一个就行。可当第三方脚本用 ObjectsDeleteAll 清场,或有人手改了对象属性,你的程序会悄悄失去控制权。提前给对象留好退路,比事后排查省心得多。

「删除事件里的防循环自恢复逻辑」

在 OnChartEvent 里处理对象删除或变更事件时,若前缀匹配且未开启防篡改与事件重置,会直接调用 RestoringObjArrayAll 把被保护对象按原参数重建,随后 return 退出分支。 当 flagResetEventDelete 大于 0 时,代码先用 StringFind(sparam,prefixObj) 定位前缀;若 findPrefixObject==0 说明是当前受保护对象被删,此时把 findPrefixObject 置 -1,并将 flagResetEventDelete 自减 1。这个自减动作是关键:它避免删除对象时事件被排进队列反复触发,导致恢复函数进入循环而写错图表状态。 下面这段是从事件分支里抽出的核心片段,注意注释里写的『重设事件计数器标志值,以避免在删除对象时事件形成队列造成循环以至于恢复工作错误』,实盘加载 EA 后手动删一个带前缀的对象,就能在日志里看到该标志逐次回落。外汇与贵金属图表对象受事件驱动,这类自恢复机制虽降低误操作概率,但高频事件下仍可能漏恢复,属高风险环境。

MQL5 / C++
   RestoringObjArrayAll(chartObject,prefixObj,flagAntivandal,
                         flagResetEventDelete,QUANT_OBJ,nameObj,
                         timeCreateObj);
         class="kw">return;
         }class=class="str">"cmt">//if(findPrefixObject==class="num">0)
      class="kw">return;
      }class=class="str">"cmt">//else if(flagAntivandal==class="num">0 && flagResetEventDelete==class="num">0)
   class="kw">return;
   }class=class="str">"cmt">//else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
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;
      }

防误删对象的函数参数与名称比对逻辑

这段函数负责在图表对象被删除事件触发时,判断被删的是否属于受保护对象集合。入参里 quant_obj 指定当前受保护对象数量,name[] 与 time_create[] 按索引一一对应,prefix_obj 用作批量删除时的前缀标识。 函数开头先做参数合法性拦截:若 quant_obj<=0 直接 Print 报错并返回 -1;若 quant_obj>quant_obj_all 且 quant_obj_all 不等于 -1 同样报错退出。当 quant_obj_all 为 -1 时,内部将其修正为 quant_obj,避免后续循环越界。 核心比对放在 for(i=quant_obj-1;i>=0;i--) 的倒序循环里,用 StringCompare(sparam,name[i],true) 做不区分大小写的全名比较。命中 comparison==0 即代表本次删除事件的对象在保护名单中,此时 res 置 1,打印恢复提示,并把 flag_antivandal 设 -1 屏蔽后续删除回调,flag_reset 设为 quant_obj_all-1 以切断事件链。 紧接着调用 ObDeletePrefixFlag 清掉数组里残留的受保护对象,再 ChartRedraw 重绘。你在 MT5 里接图表事件时,照这套参数结构抄,就能拦住脚本或人手误删画线。

MQL5 / C++
class="kw">const class="type">class="kw">string prefix_obj,
 class="type">int &flag_antivandal,
 class="type">int &flag_reset,
 class="kw">const class="type">int quant_obj,
 class="type">class="kw">string &name[],
 class="type">class="kw">datetime &time_create[],
 class="type">int quant_obj_all=-class="num">1,
 class="kw">const class="type">long chart_ID=class="num">0,
 class="kw">const class="type">int sub_window=class="num">0,
 class="kw">const class="type">int start_pos=class="num">0
 )
{
class=class="str">"cmt">//--- 接受的结果:
 class="type">int res=-class="num">1;
class=class="str">"cmt">/*检查对象数量是否正确
 这是在函数的外部参数中设定的. 如果在函数中遇到了错误的外部参数,
 就无法正确地进行"提醒"通知.
 所以您必须在代码处理之前就保证
 在函数中设置了正确的外部参数.*/
 if(quant_obj<=class="num">0)class=class="str">"cmt">//如果被保护对象的总数 <= class="num">0
  {
   Print(LINE_NUMBER,__FUNCTION__,
        ", 错误代码. 错误数值: quant_obj =",quant_obj);
   class="kw">return(res);
  }
 else if(quant_obj>quant_obj_all && quant_obj_all!=-class="num">1)
  {
   Print(LINE_NUMBER,__FUNCTION__,
        ", 错误代码. 错误数值: quant_obj_all");
   class="kw">return(res);
  }
 else if(quant_obj_all==-class="num">1){quant_obj_all=quant_obj;}
class=class="str">"cmt">//--- 用于和对象全名比较的变量:
 class="type">int comparison=-class="num">1;
class=class="str">"cmt">/*在循环中检查事件中取得的名称
与需要保护对象名称数组是否相同:*/
 for(class="type">int i=quant_obj-class="num">1;i>=class="num">0;i--)
  {
   comparison=StringCompare(sparam,name[i],true);
   class=class="str">"cmt">//--- 当名称完全匹配:
   if(comparison==class="num">0)
     {
      comparison=-class="num">1;
      res=class="num">1;
      class=class="str">"cmt">//--- 通知 "不安全" 发生的状况:
      Print(LINE_NUMBER,textRestoring,sparam);
class=class="str">"cmt">/*当程序恢复对象时,
我们设置标志以不回应删除对象的事件:*/
      flag_antivandal=-class="num">1;
class=class="str">"cmt">/*事件计数器标志的初始值
在未来的事件处理中不再需要. 对于之后的情况,
防止因为删除对象的恢复行为创建的事件序列产生代码循环:*/
      flag_reset=quant_obj_all-class="num">1;
      class=class="str">"cmt">//--- 如果数组中有要保护的对象, 将其删除:
      ObDeletePrefixFlag(flag_reset,prefix_obj,chart_ID,sub_window,start_pos);
      class=class="str">"cmt">//--- 重绘图表:
      ChartRedraw();

◍ 按前缀批量清对象时的防误删细节

在 MT5 面板类 EA 里,常需要按统一前缀(如 "Panel_")清理图表上被用户手删或重命名的图形对象。下面这段函数用 ObjectsTotal 拿到当前窗口对象总数,再倒序遍历,用 StringSubstr 截取名称指定位置的前缀做比对,命中才调 ObDelete 删除。 关键点在遍历顺序:必须从 quant_obj-1 递减到 0。因为 ObjectName 的索引在删除对象后会实时重排,正序删会漏掉一半。倒序能保证每次删完剩下的索引不受影响。 flag_reset 以引用方式传入,函数内统计实际删除数 count,若 count 大于原有标志值则刷新它。这给上层逻辑一个信号:有 N 个对象被清掉,需要重建面板。外汇与贵金属图表对象频繁变动,这类防篡改清理逻辑能降低脚本崩图概率,但 MT5 环境本身仍属高风险操作,参数误配可能清掉无关对象。 下面把核心函数逐行拆一下: int ObDeletePrefixFlag(int &flag_reset, string prefix_obj, const long chart_ID=0, const int sub_window=0, int start_pos=0) // 入参:flag_reset 引用计数、prefix_obj 前缀、chart_ID 图表ID(默认0当前图)、sub_window 窗口(默认0主图)、start_pos 前缀起始位置 { int quant_obj=ObjectsTotal(chart_ID,sub_window,-1); // 取该图表该窗口下所有类型对象的总数 if(quant_obj>0) // 有对象才进处理 { int count=0; // 本次删除计数器 int prefix_len=StringLen(prefix_obj); // 前缀字符串长度,决定截取几位 string find_substr=NULL,obj_name=NULL; // 暂存截出的子串和对象名 for(int i=quant_obj-1;i>=0;i--) // 倒序遍历,避免删后索引错位 { obj_name=ObjectName(chart_ID,i,sub_window,-1); // 取第 i 个对象名 find_substr=StringSubstr(obj_name,start_pos,prefix_len); // 从 start_pos 起截 prefix_len 长,看是不是目标前缀 if(StringCompare(find_substr,prefix_obj,true)==0) // 忽略大小写比对,相等返回0 { count=count+1; // 计数+1 ObDelete(chart_ID,obj_name); // 执行删除 } } if(count>0 && flag_reset<count) // 有删除且原标志更小 {flag_reset=count;} // 更新引用标志 } return(flag_reset); // 返回最终标志值 }

MQL5 / C++
class="type">int ObDeletePrefixFlag(class="type">int &flag_reset,
                         class="type">class="kw">string prefix_obj,
                         class="kw">const class="type">long chart_ID=class="num">0,
                         class="kw">const class="type">int sub_window=class="num">0,
                         class="type">int start_pos=class="num">0)
  {
   class="type">int quant_obj=ObjectsTotal(chart_ID,sub_window,-class="num">1);
   if(quant_obj>class="num">0)
    {
     class="type">int count=class="num">0;
     class="type">int prefix_len=StringLen(prefix_obj);
     class="type">class="kw">string find_substr=NULL,obj_name=NULL;
class=class="str">"cmt">//---
     for(class="type">int i=quant_obj-class="num">1;i>=class="num">0;i--)
      {
       obj_name=ObjectName(chart_ID,i,sub_window,-class="num">1);
       find_substr=StringSubstr(obj_name,start_pos,prefix_len);
       if(StringCompare(find_substr,prefix_obj,true)==class="num">0)
        {
         count=count+class="num">1;
         ObDelete(chart_ID,obj_name);
        }
      }
     if(count>class="num">0 && flag_reset<count)
      {flag_reset=count;}
    }
   class="kw">return(flag_reset);
  }

「在同一个指标里塞进两套自恢复逻辑」

把两种应对未授权操作的回应做进一个指标,比随机响应更可控:按步骤触发而不是碰运气。这套实现虽然附加选项简单,但暴露了竞争条件——比如按钮删除与面板最小化同时发生时的状态冲突,值得在 MT5 里单步跟一遍。 指标 id_name_object 的核心行为是:点图表上任意对象,面板显示其名称、创建时间与类型;你手动改或删这些显示值,它们会自我恢复。面板主体能最小化;若最小化按钮被删改,指标按“暂时不需要”原则自行从图表分离。对象重命名后必须删除重建,这是后两项功能落地的前提。 外部参数只暴露按钮宽、高和字体大小,面板尺寸由按钮推导,不碰代码就能放大显示。flagClick 在 OnInit 前声明为全局:0 追最小化按钮点击且主体可见时删按钮,-1 最小化/最大化切换前置位,1 追最大化按钮;它同时是主反破坏标志,0 启用主体提醒、1 启用最大化按钮提醒、-1 禁用。flagResetEventDelete 专门在自恢复时重置不需处理的删除事件。 OnInit 里先把两标志置 -1,文字数组清空,按终端语言定标题;OnChartEvent 中 flagClick=0 点最小化则置 -1、收起主体并建最大化按钮再置 1,点删除指标按钮同样先置 -1 再卸指标;flagClick=1 点最大化则置 -1、删按钮重建面板、辅助标志置 1。删除/修改通知块里,标志 -1 直接退出(友好操作),辅助标志>0 且对象属面板则重置退出,双零则匹配保护名调 RestoringObjArrayAll 恢复,等于 1 且匹配最大化按钮名则指标自离。 俄语终端显示俄文标题、其他语言英文,这是用 TerminalInfoString 判断后填的。下表是面板对象映射的实数据,开 MT5 挂上指标能对号入座:画布 nameRectLabel 用 OBJ_RECTANGLE_LABEL 背景 clrSilver;三个 OBJ_BUTTON 的 Wingdings 字符码分别是 251/225/226;六个 OBJ_EDIT 分两组存对象名、时间、类型标签与值。

MQL5 / C++
class="macro">#define CLR_PANEL                 clrSilverclass=class="str">"cmt">//面板背景色
class="macro">#define CLR_TEXT                  C&class="macro">#x27;class="num">39,class="num">39,class="num">39&class="macro">#x27;class=class="str">"cmt">//主文字色
class="macro">#define CLR_TEXT_BACK             C&class="macro">#x27;class="num">150,class="num">150,class="num">150&class="macro">#x27; class=class="str">"cmt">//影子文字颜色 
class="macro">#define CLR_BORDER                clrLavenderclass=class="str">"cmt">//边框颜色
class="type">uchar textButtUchar[class="num">3]={class="num">251,class="num">225,class="num">226};
class="type">int flagClick, flagResetEventDelete;
class="type">int OnInit()
  {
   flagClick=flagResetEventDelete=-class="num">1;
class=class="str">"cmt">/*面板文字数组
   用于显示点击的图形对象属性内容:*/
   for(class="type">int i=QUANT_OBJ_EDIT-class="num">1;i>=class="num">0;i--){textEdit1[i]=" ";}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">/*指标的短名称:*/

控制面板的初始化与画布生成

在自定义指标里给面板起名,得把品种和周期拼进短名,否则多图表加载时对象会互相覆盖。下面这段代码用 StringConcatenate 把前缀、指标名、Symbol() 和 Period() 的枚举字符串接起来,再写进 INDICATOR_SHORTNAME,MT5 里切到任意货币对或周期都能看到独立标签。 按钮尺寸不能放任用户输入,代码里把 objWidthHeight 夹在 20 到 300 像素之间:小于 20 按 20 处理,大于 300 直接锁 300。这个硬限制能避免面板在 4K 屏或小窗里变形,实盘加载前建议先改改这个区间看看观感。 面板画布宽高按控件尺寸推算:widthPanel = obj0WidthHeight*11 + 间距*2,heightPanel = obj0WidthHeight*6 + 间距*8。若按默认间距算,obj0WidthHeight 取 30 时画布约 370×340,加载后可用 ObjectFind 查 nameRectLabel 是否存在来确认只建了一次。 初始化末尾挂了 8 秒定时器(EventSetTimer(8)),并打开了图表对象删除事件通知。若 RevisionCreateTime 记录按钮和编辑框的创建时间失败,直接返回 REASON_INITFAILED,这时 MT5 终端会报初始化错误,得去检查对象名前缀是否撞车。

MQL5 / C++
  StringConcatenate(prefixObj,INDICATOR_NAME,"_",Symbol(),"_",
                     EnumToString(Period()));
  IndicatorSetString(INDICATOR_SHORTNAME,prefixObj);
class=class="str">"cmt">//--- 创建对象的名称
  NameObjectPanel(nameRectLabel,prefixObj,"rl");
  NameObjectPanel(QUANT_OBJ_BUTTON,nameButton,prefixObj,"but");
  NameObjectPanel(QUANT_OBJ_EDIT,nameEdit0,prefixObj,"ed0");
  NameObjectPanel(QUANT_OBJ_EDIT,nameEdit1,prefixObj,"ed1");
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">/*指标的文字, 依赖于交易终端的语言:*/
  LanguageTerminal();
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">/*限制按钮的宽度和高度:*/
  if(objWidthHeight>=class="num">20 && objWidthHeight<=class="num">300)
     {obj0WidthHeight=objWidthHeight;}
  else if(objWidthHeight>class="num">300){obj0WidthHeight=class="num">300;}
  else {obj0WidthHeight=class="num">20;}
class=class="str">"cmt">//--- 在图表上创建控制面板
  PanelCreate();
class=class="str">"cmt">/*用于保存和修改创建对象时间的数组,
如果出错则退出程序:
(面板的主体部分三个按钮只会显示两个,
所以在有变化的时候它们的数量会减1)*/
  if(!RevisionCreateTime(QUANT_OBJ_BUTTON-class="num">1,timeCreateButton))
     {class="kw">return(REASON_INITFAILED);}
  if(!RevisionCreateTime(QUANT_OBJ_EDIT,timeCreateEdit0))
     {class="kw">return(REASON_INITFAILED);}
  if(!RevisionCreateTime(QUANT_OBJ_EDIT,timeCreateEdit1))
     {class="kw">return(REASON_INITFAILED);}
class=class="str">"cmt">//---
  flagClick=flagResetEventDelete=class="num">0;
class=class="str">"cmt">//--- 包含删除图形对象事件的通知:
  class="type">bool res;
  ChartEventObjectDeleteGet(res);
  if(res!=true)
     {ChartEventObjectDeleteSet(true);}
class=class="str">"cmt">//--- 创建计时器
  EventSetTimer(class="num">8);
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void PanelCreate(class="kw">const class="type">long chart_ID=class="num">0,class=class="str">"cmt">//chart&class="macro">#x27;s ID 
                  class="kw">const class="type">int sub_window=class="num">0)class=class="str">"cmt">// subwindow&class="macro">#x27;s number
  {
class=class="str">"cmt">//--- 面板画布的宽度:
  class="type">int widthPanel=(obj0WidthHeight*class="num">11)+(CONTROLS_GAP_XY*class="num">2);
class=class="str">"cmt">//--- 面板画布的高度:
  class="type">int heightPanel=(obj0WidthHeight*class="num">6)+(CONTROLS_GAP_XY*class="num">8);
class=class="str">"cmt">//--- x轴上的距离:
  class="type">int x_dist=X_DISTANCE+widthPanel;
class=class="str">"cmt">//---
  if(ObjectFind(chart_ID,nameRectLabel)<class="num">0)class=class="str">"cmt">//canvas
     {
     RectLabelCreate(chart_ID,nameRectLabel,sub_window,x_dist,
                      Y_DISTANCE,widthPanel,heightPanel,"\n",CLR_PANEL,
                      BORDER_RAISED,CORNER_PANEL,clrNONE,STYLE_SOLID,class="num">1,
                      class="kw">false,class="kw">false,true);
     class=class="str">"cmt">//--- 取得并记录创建对象的时间:
     CreateTimeGet(chart_ID,nameRectLabel,timeCreateRectLabel);
     }
class=class="str">"cmt">//---
  x_dist=X_DISTANCE+CONTROLS_GAP_XY;
  class="type">int y_dist=Y_DISTANCE+CONTROLS_GAP_XY;
class=class="str">"cmt">//---
  for(class="type">int i=QUANT_OBJ_BUTTON-class="num">2;i>=class="num">0;i--)
     {
     class=class="str">"cmt">//--- 用于删除和最小化指标的按钮:

◍ 面板控件按坐标批量落位

这段逻辑干的事很直白:在图表子窗口里按相对坐标把按钮和编辑框画出来,省得手动拖。按钮循环先查 ObjectFind,找不到才用 ButtonCreate 建,x 坐标是 X_DISTANCE+widthPanel-CONTROLS_GAP_XY 再向右推 obj0WidthHeight*(i+1),字体用 Wingdings 配合 CharToString 把字符变成图标符号。 编辑框分两列:nameEdit0 放属性名,宽度 obj0WidthHeight*8、ALIGN_CENTER 居中;nameEdit1 放属性值,宽度 obj0WidthHeight*11、ALIGN_LEFT 左对齐。两列都从右上角 CORNER_RIGHT_UPPER 锚定,y 方向按 y_dist_plus = CONTROLS_GAP_XY*2 + obj0WidthHeight*2 逐行下移,倒序循环 i 从 QUANT_OBJ_EDIT-1 到 0。 每个对象建完都调 CreateTimeGet 记创建时间到对应数组,后面做点击时效判断用得上。OnDeinit 里先置 flagClick=-1,ObjectsDeleteAll 按 prefixObj 前缀清场,EventKillTimer 停定时器;若 reason 是 REASON_INITFAILED 则走删除指标分支,避免半初始化对象残留。外汇与贵金属图表加载这类自定义面板属于高风险环境操作,参数坐标算错可能让控件飞出可视区。

MQL5 / C++
if(ObjectFind(chart_ID,nameButton[i])<class="num">0)
  {
  ButtonCreate(chart_ID,nameButton[i],sub_window,
                x_dist+(obj0WidthHeight*(i+class="num">1)),y_dist,obj0WidthHeight,
                obj0WidthHeight,CORNER_PANEL,
                CharToString(textButtUchar[i]),"\n","Wingdings",
                font_sz+class="num">1,CLR_TEXT,CLR_PANEL,clrNONE);
  class=class="str">"cmt">//--- 取得并记录创建对象的时间:
  CreateTimeGet(chart_ID,nameButton[i],timeCreateButton[i]);
  }
 }
class=class="str">"cmt">//---
 x_dist=X_DISTANCE+widthPanel-CONTROLS_GAP_XY;
 class="type">int y_dist_plus=(CONTROLS_GAP_XY*class="num">2)+(obj0WidthHeight*class="num">2);
class=class="str">"cmt">//---
 for(class="type">int i=QUANT_OBJ_EDIT-class="num">1;i>=class="num">0;i--)
  {
  class=class="str">"cmt">//--- 对象属性的名称:
  if(ObjectFind(chart_ID,nameEdit0[i])<class="num">0)
    {
    EditCreate(chart_ID,nameEdit0[i],sub_window,x_dist,
               y_dist+(y_dist_plus*i),obj0WidthHeight*class="num">8,
               obj0WidthHeight,textEdit0[i],"Arial",font_sz,
               "\n",ALIGN_CENTER,true,CORNER_RIGHT_UPPER,
               CLR_TEXT_BACK,CLR_PANEL,CLR_BORDER);
    class=class="str">"cmt">//--- 取得并记录创建对象的时间:
    CreateTimeGet(chart_ID,nameEdit0[i],timeCreateEdit0[i]);
    }
  }
class=class="str">"cmt">//---
 y_dist=Y_DISTANCE+obj0WidthHeight+(CONTROLS_GAP_XY*class="num">2);
class=class="str">"cmt">//---
 for(class="type">int i=QUANT_OBJ_EDIT-class="num">1;i>=class="num">0;i--)
  {
  class=class="str">"cmt">//--- 用于显示对象属性值的文字:
  if(ObjectFind(chart_ID,nameEdit1[i])<class="num">0)
    {
    EditCreate(chart_ID,nameEdit1[i],sub_window,x_dist,
               y_dist+(y_dist_plus*i),obj0WidthHeight*class="num">11,
               obj0WidthHeight,textEdit1[i],"Arial",font_sz,
               "\n",ALIGN_LEFT,true,CORNER_RIGHT_UPPER,
               CLR_TEXT,CLR_PANEL,CLR_BORDER);
    class=class="str">"cmt">//--- 取得并记录创建对象的时间:
    CreateTimeGet(chart_ID,nameEdit1[i],timeCreateEdit1[i]);
    }
  }
 class="kw">return;
 }
class="type">void OnDeinit(class="kw">const class="type">int reason)
 {
  flagClick=-class="num">1;
  ObjectsDeleteAll(class="num">0,prefixObj,class="num">0,-class="num">1);
  EventKillTimer();
class=class="str">"cmt">//--- 如果在OnInit函数中收到 REASON_INITFAILED, 就删除指标.
  if(reason==REASON_INITFAILED)
    {

「控制面板按钮的点击分支处理」

在 MT5 的 EA 或指标里,图表对象点击通过 CHARTEVENT_OBJECT_CLICK(id=1)回调捕获。代码先用 StringFind 比对对象名前缀,只有带面板前缀的点击才进入按钮逻辑,否则走 SetValuesTable 设置普通对象值。 面板主显状态下 flagClick==0,此时用 for 循环从 1 到 0 比对 nameButton 数组。命中「最小化」按钮(case 1)会调用 MinimizeTable 把面板收成两条文本;命中「删除指标」按钮(case 0)则调 ChIndicatorDelete 清掉前缀对应的指标对象,这两个分支都先置 flagClick=-1 防止重入。 OnTimer 里用 ChartEventObjectDeleteGet/Set 记录删除事件,避免对象被手动删时状态错乱。外汇与贵金属图表上挂这类面板需注意:对象频繁重绘会带来额外 CPU 开销,低频品种上可能倾向忽略部分实时刷新。 下面这段是点击分发与定时清理的核心片段,可直接粘进 mq5 的事件函数区验证:

MQL5 / C++
ChIndicatorDelete(prefixObj,textDelInd[class="num">0],textDelInd[class="num">1],class="num">0,class="num">0);
   }
class=class="str">"cmt">//---
   ChartRedraw();
   }
class="type">void OnTimer()
  {
class=class="str">"cmt">//--- 包含关于删除图形对象事件的通知
   class="type">bool res;
   ChartEventObjectDeleteGet(res);
   if(res!=true)
    {ChartEventObjectDeleteSet(true);}
   }
if(id==CHARTEVENT_OBJECT_CLICK)class=class="str">"cmt">//id = class="num">1
    {
     if(flagClick==class="num">0)class=class="str">"cmt">//如果控制面板主要部分在图表上
      {
       class="type">class="kw">string clickedChartObject=sparam;
       findPrefixObject=StringFind(clickedChartObject,prefixObj);
       class=class="str">"cmt">//---
       if(findPrefixObject==class="num">0)
        {
         findPrefixObject=-class="num">1;
         class=class="str">"cmt">//---
         for(class="type">int i=class="num">1;i>=class="num">0;i--)
          {
           if(StringCompare(clickedChartObject,nameButton[i],true)==class="num">0)
            {
             class="kw">switch(i)
              {
               class=class="str">"cmt">//--- "最小化" 按钮:
               case class="num">1:flagClick=-class="num">1;
               MinimizeTable(nameButton,class="num">2,textButtUchar,class="num">2,
                  timeCreateButton,obj0WidthHeight);
               flagClick=class="num">1;class="kw">return;
               class=class="str">"cmt">//--- 用于从图表上删除指标的按钮:
               case class="num">0:flagClick=-class="num">1;
               ChIndicatorDelete(prefixObj,textDelInd[class="num">0],textDelInd[class="num">1],class="num">0,class="num">0);
               class="kw">return;
               class="kw">default:class="kw">return;
              }
            }
          }
        }
       elseclass=class="str">"cmt">//如果没有点击在控制面板的按钮上,
        {
         SetValuesTable(clickedChartObject);class=class="str">"cmt">//我们设置对象的值
         class="kw">return;
        }
      }
class=class="str">"cmt">//--- 如果点击了 "最大化" 按钮:

图表对象点击与编辑事件的拦截逻辑

在 MT5 的 OnChartEvent 回调里,图表对象的交互主要靠 CHARTEVENT_OBJECT_CLICK(点击)和 CHARTEVENT_OBJECT_ENDEDIT(结束编辑)两类事件驱动。上面这段处理代码先判断 flagClick==1,再用 StringFind 以 prefixObj 前缀过滤出属于自己的对象,避免误响应其他脚本画的线。 点击分支中,若命中 nameButton[2](即“最大化”面板按钮,忽略大小写比对),会先把 flagClick 置 -1 防重入,调用 ToExpandTheTable 展开面板,随后复位 flagClick=0 并置 flagResetEventDelete=1 以清除删除事件,最后 return 截断后续逻辑。 编辑分支监听 id==CHARTEVENT_OBJECT_ENDEDIT(事件号 3),同样用前缀定位。它倒序遍历 QUANT_OBJ_EDIT 个编辑框,若 sparam 与 nameEdit1[i] 匹配,就读取新文本 textNew 并与旧值 textEdit1[i] 比较;不一致时强行用 ObSetString 把文本写回原值,等于锁死用户输入,再 ChartRedraw 刷新。外汇与贵金属图表上挂这类 EA 属于高风险操作,参数锁死可能影响手动标注效率,建议在模拟账户先验证。

MQL5 / C++
else if(flagClick==class="num">1)
  {
   findPrefixObject=StringFind(sparam,prefixObj);
   if(findPrefixObject==class="num">0)
     {
      class="type">class="kw">string clickedChartObject=sparam;
      findPrefixObject=-class="num">1;
      class=class="str">"cmt">//--- "最大化"面板按钮:
      if(StringCompare(clickedChartObject,nameButton[class="num">2],true)==class="num">0)
        {
         flagClick=-class="num">1;
         class=class="str">"cmt">//--- 当按下按钮"最大化"面板的操作
         ToExpandTheTable(clickedChartObject);
         class=class="str">"cmt">//---
         flagClick=class="num">0;
         class=class="str">"cmt">//--- 重设删除此按钮的事件:
         flagResetEventDelete=class="num">1;
         class=class="str">"cmt">//---
         class="kw">return;
        }
     }
   class="kw">return;
  }
 class="kw">return;
  }class=class="str">"cmt">//if(id==CHARTEVENT_OBJECT_CLICK)
else  if(id==CHARTEVENT_OBJECT_ENDEDIT)class=class="str">"cmt">//id = class="num">3
   {
    findPrefixObject=StringFind(sparam,prefixObj);
    if(findPrefixObject==class="num">0)
      {
       class="type">class="kw">string endEditChartObject=sparam;
       findPrefixObject=-class="num">1;
       class="type">int comparison=-class="num">1;
       class=class="str">"cmt">//---
       for(class="type">int i=QUANT_OBJ_EDIT-class="num">1;i>=class="num">0;i--)
         {
          if(StringCompare(endEditChartObject,nameEdit1[i],true)==class="num">0)
            {
             class="type">class="kw">string textNew=ObjectGetString(class="num">0,endEditChartObject,OBJPROP_TEXT);
             comparison=StringCompare(textEdit1[i],textNew,true);
             class=class="str">"cmt">//---
             if(comparison!=class="num">0)
               {
                ObSetString(class="num">0,nameEdit1[i],OBJPROP_TEXT,textEdit1[i]);
               }
             class=class="str">"cmt">//---
             ChartRedraw();
             class="kw">return;
            }

◍ 图表对象被删时如何自动还原

在 MT5 的图表事件回调里,当 idCHARTEVENT_OBJECT_DELETECHARTEVENT_OBJECT_CHANGE 时,先要用 flagClickflagResetEventDelete 两个状态量判断当前是否处于合法交互期。若 flagClick==-1 直接返回,说明面板还没激活;若 flagResetEventDelete>0 则走防抖逻辑,用 StringFind(sparam,prefixObj) 确认被删对象是否带自己的前缀,带前缀就递减计数器并跳过,避免循环触发。 当 flagClick==0flagResetEventDelete==0 时,才真正检查用户误删。此时同样用 StringFind 定位前缀为 0 的对象,取出 sparam 作为 chartObject,依次调用 RestoringObjOneAll(还原面板矩形标签)和两次 RestoringObjArrayAll(分别还原按钮数组与 Edit0 注释栏),传入 QUANT_OBJ_ALL 作为全量还原上限。任一函数返回 1 即视为还原成功并 return。 这套机制的实际价值在于:外汇与贵金属图表上手动拖错或误删控件是高概率事件,面板对象带统一前缀(prefixObj)后,可在不刷新 EA 的前提下原位重建。注意 MT5 图表对象操作属高风险自定义行为,误写前缀可能吞掉其他指标对象,上机前务必在策略测试器用样本品种跑一遍删除事件。

MQL5 / C++
     }
class=class="str">"cmt">//for(class="type">int i=class="num">0;i<QUANT_OBJ_EDIT;i++)
     class="kw">return;
     }
class=class="str">"cmt">//if(findPrefixObject==class="num">0)
     class="kw">return;
   }
class=class="str">"cmt">//if(id==CHARTEVENT_OBJECT_ENDEDIT)
else   if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
   {
    if(flagClick==-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;
         flagResetEventDelete=flagResetEventDelete-class="num">1;
         class=class="str">"cmt">//---
         class="kw">return;
        }
      class="kw">return;
     }
    class=class="str">"cmt">//--- 检查我们的对象是否被删除:
    else if(flagClick==class="num">0 && flagResetEventDelete==class="num">0)
     {
      findPrefixObject=StringFind(sparam,prefixObj);
      class=class="str">"cmt">//---
      if(findPrefixObject==class="num">0)
        {
         class="type">class="kw">string chartObject=sparam;
         findPrefixObject=-class="num">1;
         class="type">int res=-class="num">1;
         class=class="str">"cmt">//--- 如果对象在控制面板上绘制的操作:
         res=RestoringObjOneAll(chartObject,prefixObj,flagClick,
                                 flagResetEventDelete,nameRectLabel,
                                 timeCreateRectLabel,QUANT_OBJ_ALL);
         class=class="str">"cmt">//---
         if(res==class="num">1){class="kw">return;}
         class=class="str">"cmt">//--- 如果对象是控制面板上按钮的操作:
         res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                                   flagResetEventDelete,QUANT_OBJ_BUTTON,
                                   nameButton,timeCreateButton,QUANT_OBJ_ALL);
         class=class="str">"cmt">//---
         if(res==class="num">1){class="kw">return;}
         class=class="str">"cmt">//--- 如果对象是控制面板注释栏位(Edit0)的操作:
         res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                                   flagResetEventDelete,QUANT_OBJ_EDIT,
                                   nameEdit0,timeCreateEdit0,QUANT_OBJ_ALL);

「面板对象被删改时的恢复与防误删逻辑」

在 MT5 自定义控制面板里,用户误删或改动图形对象(Edit 栏、按钮等)是高频事故。这段事件处理代码的核心思路是:当图表事件命中受保护前缀的对象时,先尝试用 RestoringObjArrayAll 把整组对象按原参数还原,避免单个控件丢失导致面板失效。 若 flagClick==1 且被点击的是 nameButton[2] 这个最小化按钮,程序会把 flagClick 置为 -1,并调用 ChIndicatorDelete 清掉关联指标后直接 return,等于把「最小化」动作转成「卸载指标」的硬处理。外汇与贵金属图表上挂这类面板时,误触可能引发持仓辅助线消失,属高风险操作。 RestoringObjOneAll 的函数签名暴露了恢复机制的底层约定:sparam 是 OnChartEvent 传来的对象名,prefix_obj 是受保护对象统一前缀,flag_antivandal 控制是否弹警告,quant_obj_all 是可恢复对象总数。把这些参数在 EA 初始化时打印一次,就能在 MT5 专家日志里核对面板对象是否全被纳入保护。

MQL5 / C++
  class=class="str">"cmt">//---
  if(res==class="num">1){class="kw">return;}
  class=class="str">"cmt">//--- 如果对象是控制面板上注释栏位(Edit1)的操作:
  res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                           flagResetEventDelete,QUANT_OBJ_EDIT,
                           nameEdit1,timeCreateEdit1,QUANT_OBJ_ALL);
  class=class="str">"cmt">//---
  class="kw">return;
   }
  }
  else if(flagClick==class="num">1)class=class="str">"cmt">//如果按钮在最小化的面板上被修改或者删除
   {
    class="type">class="kw">string clickedChartObject=sparam;
    if(StringCompare(clickedChartObject,nameButton[class="num">2],true)==class="num">0)
     {
      flagClick=-class="num">1;
      class=class="str">"cmt">//--- 从图表上删除指标:
      ChIndicatorDelete(prefixObj,textDelInd[class="num">0],textDelInd[class="num">1],class="num">0,class="num">0);
      class="kw">return;
     }
    class="kw">return;
   }
  class="kw">return;
  }class=class="str">"cmt">//else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//|class="type">class="kw">string sparam = 传给 OnChartEvent() 的对象名称                       |
class=class="str">"cmt">//|class="type">class="kw">string prefix_obj = 被保护对象的通用前缀                            |
class=class="str">"cmt">//|class="type">int &flag_antivandal = 启用/禁用警告的标志                           |
class=class="str">"cmt">//|class="type">int &flag_reset = 重设事件的标志                                     |
class=class="str">"cmt">//|class="type">class="kw">string name = 对象名称                                               |
class=class="str">"cmt">//|class="type">class="kw">datetime time_create = 对象创建时间                                  |
class=class="str">"cmt">//|class="type">int quant_obj_all = 程序中可恢复对象的总数                          |
class=class="str">"cmt">//|>= quant_obj(如果是-class="num">1, 则等于 quant_obj)                            |
class=class="str">"cmt">//|class="kw">const class="type">long chart_ID = class="num">0 图表id                                       |
class=class="str">"cmt">//|class="kw">const class="type">int sub_window = class="num">0 窗口索引                                    |
class=class="str">"cmt">//|class="type">int start_pos=class="num">0 寻找前缀的对象名称位置 |
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">int RestoringObjOneAll(class="kw">const class="type">class="kw">string sparam,
                       class="kw">const class="type">class="kw">string prefix_obj,
                       class="type">int &flag_antivandal,

图形对象防篡改的匹配与复位逻辑

在 MT5 自定义指标或 EA 里,用前缀标记批量管理图形对象时,最怕用户手动删了关键控件导致面板失灵。下面这段函数通过名称比对来识别「非法操作」并自动复位。 函数入参里 name 是待校验对象名,sparam 是触发事件的图形名,quant_obj_all 记录受管对象总数,chart_ID 与 sub_window 默认指向主图(0,0)。time_create 用于保留初始创建时间戳,start_pos 控制遍历起点。 核心判断只有一句:StringCompare(sparam,name,true) 返回 0 表示名称完全一致(忽略大小写)。此时 res 置 1,打印 LINE_NUMBER 与 textRestoring 提示不安全状况,并把 flag_antivandal 打为 -1 暂锁。 随后 flag_reset 被设为 quant_obj_all-1,调用 ObDeletePrefixFlag 按前缀清掉残留对象,两次 ChartRedraw 强制重绘,ObDelCreateTimeExcept0 删除非初始时间的对象,再 PanelCreate 重建面板,最后解锁 flag_antivandal=0 并返回。外汇与贵金属图表上跑这套逻辑须留意:高频手动拖拽可能触发连续复位,实盘前请在模拟盘验证对象数量与帧率开销。

MQL5 / C++
class="type">int &flag_reset,
class="type">class="kw">string name,
class="type">class="kw">datetime time_create,
class="kw">const class="type">int quant_obj_all,
class="kw">const class="type">long chart_ID=class="num">0,
class="kw">const class="type">int sub_window=class="num">0,
class="type">int start_pos=class="num">0
)
{
 class="type">int res=-class="num">1;
 class="type">int comparison=-class="num">1;
class=class="str">"cmt">//---
 comparison=StringCompare(sparam,name,true);
class=class="str">"cmt">//--- 当名称完全匹配时:
 if(comparison==class="num">0)
  {
   res=class="num">1;
   comparison=-class="num">1;
   class=class="str">"cmt">//--- 通知 "不安全" 发生的状况:
   Print(LINE_NUMBER,textRestoring,sparam);
   class=class="str">"cmt">//---
   flag_antivandal=-class="num">1;
   flag_reset=quant_obj_all-class="num">1;
   class=class="str">"cmt">//---
   ObDeletePrefixFlag(flag_reset,prefix_obj,chart_ID,sub_window,start_pos);
   class=class="str">"cmt">//---
   ChartRedraw();
   class=class="str">"cmt">//---
   ObDelCreateTimeExcept0(time_create,chart_ID,sub_window,-class="num">1);
   class=class="str">"cmt">//---
   PanelCreate();
   class=class="str">"cmt">//---
   ChartRedraw();
   class=class="str">"cmt">//---
   flag_antivandal=class="num">0;
   class=class="str">"cmt">//---
   class="kw">return(res);
  }
 class="kw">return(res);
}

◍ 把防破坏框架接进你已有的EA

上面这套基于标记的图形对象防护思路,可以直接塞进写好的MQL5代码里,不用大改主逻辑,也不太会拖慢程序跑图。多个项目要用的话,按需要挂一套或几套都行。 真碰上乱改面板对象的情况,比如别的程序误删了你的画线,可以在计时器里加个计数器,专门统计重要对象被改的次数。EA交易里若不影响下单效率,遇到未授权的对象删除就弹个动态提示,顺便给两个按钮:从图表卸掉程序,或尝试复原被破坏的对象。 计数器可以这么写提示逻辑:这是程序对象第XX次被干扰,到XX次后程序自动脱图,请自查原因。作者在MetaEditor 5.00 build 1241里重编了test_count_click_0/1/2.mq5,防破坏功能仍正常,但发现一个非防护函数在新版编译出问题,已提交工单#1379624(假期无回复)。外汇和贵金属图形对象防护涉及自动退出,行情波动大时误触可能断策略,属高风险操作,参数阈值自己压着测。

「把工具请下神坛」

回到 1241 编译器的坑:在 OnChartEvent() 的 CHARTEVENT_OBJECT_CLICK 分支里,若直接对 countClick 调 ArrayFill() 而不先输出或重绘,除首个按钮外其余点击计数会卡死在同一数值。把 ChartRedraw() 或打印语句塞进 ArrayFill() 之前,编译后行为即恢复正常。 下面三段代码逐行拆开看,差异只在防破坏策略: count=countClick[index]+count; int summ=countClick[NUMBER_ALL]+1; //--- ArrayFill(countClick,index,1,count); ArrayFill(countClick,NUMBER_ALL,1,summ) 上面这段是出问题的原写法——先算 count 和 summ,紧接着无前置刷新就填数组,导致后续点击不更新。 count=countClick[index]+count; int summ=countClick[NUMBER_ALL]+1; //--- //TEST_PRINT_TWO(count,summ); ChartRedraw(); //--- ArrayFill(countClick,index,1,count); ArrayFill(countClick,NUMBER_ALL,1,summ); 修复版在 ArrayFill() 前插入 ChartRedraw(),强制图表重绘,点击计数随即正确累加。 test_count_click_0 不带防破坏;_1 遇未授权干扰会自删;_2 则尝试自恢复。三者都依赖 Include 下的 objectcreateandset.mqqh,缺了文件代码跑不起来。顺带提一句,StringFind() 做的名称过滤器能砍掉大量对象遍历,不光防破坏时用得上。 神经网络、马丁格尔那些被吹上天的模块,本质和这套点击计数一样,只是终端里的一段逻辑。外汇和贵金属杠杆高、滑点狠,任何指标或 EA 都只是帮你少点几次鼠标的扳手——真要信神,先开 MT5 把上面三段编译跑一遍再说。

MQL5 / C++
               count=countClick[index]+count;
			   class="type">int summ=countClick[NUMBER_ALL]+class="num">1;
			   class=class="str">"cmt">//---
			   ArrayFill(countClick,index,class="num">1,count);
			   ArrayFill(countClick,NUMBER_ALL,class="num">1,summ)

               count=countClick[index]+count;
			   class="type">int summ=countClick[NUMBER_ALL]+class="num">1;
			   class=class="str">"cmt">//---
			   class=class="str">"cmt">//TEST_PRINT_TWO(count,summ);
			   ChartRedraw();
			   class=class="str">"cmt">//---
			   ArrayFill(countClick,index,class="num">1,count);
			   ArrayFill(countClick,NUMBER_ALL,class="num">1,summ);
把对象巡检交给小布盯盘
这些对象丢失与自愈的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到异常对象提示,你只管判断要不要干预。

常见问题

在 CHARTEVENT_OBJECT_DELETE 分支里用 id 参数比对对象名或前缀,未匹配到你程序前缀的可以忽略,匹配到的再触发恢复逻辑。
终端首要解决交易者问题,武断拦截会干扰其他程序或人工操作,类似清洁工碰掉电脑就把人扔窗外,并不合理也得不到好处。
如果没限定前缀或类型,它默认清掉同窗口全部对象,所以程序里最好用带前缀的删除函数,并为关键对象留恢复分支。
可以,小布盯盘会比对对象命名规则与存活状态,发现前缀错乱或属性被改就标出,省去你手动翻图表对象列表。
自我分离适合退出时清理无主对象避免污染,自我恢复适合对象被误删后原地重建,实例里把两者放在一个程序内便于按需切换。