MQL5 初学者: 图形对象的防破坏保护·综合运用
(3/3)·当别人或第三方脚本误删你的控制面板对象,程序如何不失控且不留无主残影
「删除事件里的防循环自恢复逻辑」
在 OnChartEvent 里处理对象删除或变更事件时,若前缀匹配且未开启防篡改与事件重置,会直接调用 RestoringObjArrayAll 把被保护对象按原参数重建,随后 return 退出分支。 当 flagResetEventDelete 大于 0 时,代码先用 StringFind(sparam,prefixObj) 定位前缀;若 findPrefixObject==0 说明是当前受保护对象被删,此时把 findPrefixObject 置 -1,并将 flagResetEventDelete 自减 1。这个自减动作是关键:它避免删除对象时事件被排进队列反复触发,导致恢复函数进入循环而写错图表状态。 下面这段是从事件分支里抽出的核心片段,注意注释里写的『重设事件计数器标志值,以避免在删除对象时事件形成队列造成循环以至于恢复工作错误』,实盘加载 EA 后手动删一个带前缀的对象,就能在日志里看到该标志逐次回落。外汇与贵金属图表对象受事件驱动,这类自恢复机制虽降低误操作概率,但高频事件下仍可能漏恢复,属高风险环境。
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 里接图表事件时,照这套参数结构抄,就能拦住脚本或人手误删画线。
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); // 返回最终标志值 }
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 分两组存对象名、时间、类型标签与值。
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 终端会报初始化错误,得去检查对象名前缀是否撞车。
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 则走删除指标分支,避免半初始化对象残留。外汇与贵金属图表加载这类自定义面板属于高风险环境操作,参数坐标算错可能让控件飞出可视区。
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 的事件函数区验证:
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 属于高风险操作,参数锁死可能影响手动标注效率,建议在模拟账户先验证。
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 的图表事件回调里,当 id 为 CHARTEVENT_OBJECT_DELETE 或 CHARTEVENT_OBJECT_CHANGE 时,先要用 flagClick 和 flagResetEventDelete 两个状态量判断当前是否处于合法交互期。若 flagClick==-1 直接返回,说明面板还没激活;若 flagResetEventDelete>0 则走防抖逻辑,用 StringFind(sparam,prefixObj) 确认被删对象是否带自己的前缀,带前缀就递减计数器并跳过,避免循环触发。
当 flagClick==0 且 flagResetEventDelete==0 时,才真正检查用户误删。此时同样用 StringFind 定位前缀为 0 的对象,取出 sparam 作为 chartObject,依次调用 RestoringObjOneAll(还原面板矩形标签)和两次 RestoringObjArrayAll(分别还原按钮数组与 Edit0 注释栏),传入 QUANT_OBJ_ALL 作为全量还原上限。任一函数返回 1 即视为还原成功并 return。
这套机制的实际价值在于:外汇与贵金属图表上手动拖错或误删控件是高概率事件,面板对象带统一前缀(prefixObj)后,可在不刷新 EA 的前提下原位重建。注意 MT5 图表对象操作属高风险自定义行为,误写前缀可能吞掉其他指标对象,上机前务必在策略测试器用样本品种跑一遍删除事件。
} 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 专家日志里核对面板对象是否全被纳入保护。
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 并返回。外汇与贵金属图表上跑这套逻辑须留意:高频手动拖拽可能触发连续复位,实盘前请在模拟盘验证对象数量与帧率开销。
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 把上面三段编译跑一遍再说。
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);