MQL5 初学者: 图形对象的防破坏保护

Dina Paches | 29 二月, 2016

蓄意破坏是人们的一种越轨行为,
是对文化, 艺术对象, 公有或者私有的
财产被破坏或者亵渎.

维基百科

内容:


1. 简介

MQL5编程语言的一个优点是, 使用MQL5的标准函数, 您可以在使用MetaTrader 5交易终端时使用代码完成各种任务以及达到各种目标.

这篇文章使用简明的语言, 易懂的实例, 使用两种不同方式来实现控制面板在其图形对象被删除或者修改时的回应. 我们将会发现, 您如何确认在应用程序退出之后, 在图表上不会出现无主的对象, 这样的对象可能是某人或者程序把它们重新命名而使得程序对之失去了控制.

控制面板在对象属性被人工修改前后的实例

图 1. 控制面板在其对象属性被人工修改前后的外观实例

在本文中描述的, 构造对来自外界干扰的回应的代码, 对于其他情况也并非是多余的. 举例来说, 在图表上运行的第三方程序没有准备做清除的, 使用某些参数调用一个函数来删除对象(ObjectsDeleteAll () 或者您自己创建的函数):

这些选项在一定条件下都是合理的, 包括程序正确运行中, 在意外或者有意删除了控制面板对象时, 或者在代码中人工修改了它们的属性时提供可以执行的操做.

本文对刚刚开始学习OnChartEvent()函数的事件处理的开发者也会有所帮助.

我想直接提醒您, 本文不包括创建"强硬"的回应代码, 就是那种对象会在未经授权的条件下修改或者删除的代码. 终端程序的主要目的是解决交易者可能遇到的问题, 所以武断地干预是不可接受的.

对于喜欢强硬行动的人, 我建议您在考虑这样做之前做一个下面的类比. 假如有个办公室的清洁工, 在用抹布擦桌子的时碰掉了桌上的电脑, 或者在某人画出漂亮的作品时弄脏了他/她的桌子或者电脑. 所以如果被毁坏财物的主人的反应是, 在大庭广众之下把电脑, 家具连同清洁工一起扔出窗外, 他的行为显然是不当的. 另外, 做出这种激进行为的人也得不到什么好处.

在继续进行对蓄意破坏对象回应的两种(从更多选择中选出的)可能的方法之前, 我相信有必要提一下在MQL5/MQL4编程语言中的一种对象保护方法.


2. 在 MQL5/MQL4 中重点对象的保护

如果没有这种保护, 对程序创建对象属性的访问就会更加开放.

我来向您解释一下我的意思. 为了保护对象, 防止它们被删除或者修改属性 - 名称, 描述, 颜色等等. 提供了一个OBJPROP_HIDDEN特性, 可以被明确使用. 它可以设置障碍, 使得图形对象的名称在终端的对象列表菜单中不显示: 图表 -> 对象 -> 对象列表. 默认情况下, 显示日历事件的对象, 交易历史和MQL5程序所创建的对象都设置了这一选项 .

明确设置禁止(没有默认设置) 可以在代码中如下实现:

ObjectSetInteger(chart_id,name,OBJPROP_HIDDEN,true);

其中:

这种实现方式以及相关代码可以在文档中找到. 通过点击提供的对象类型列表中的任意链接, 您就可以看到使用函数创建对象的例子代码.

在图形对象名称列表中禁止显示的对象, 不论是明确说明还是默认的, 都可以通过按下全部按钮来显示图表上的全部对象来看到. 对于通过对象列表人工干预图形对象的属性来说这只是初级保护.

在按下 全部按钮之前图表上的对象列表

图 2. 在按下"全部"按钮之前的图表上的对象列表

在按下全部按钮之后的图表上的对象列表

图 3. 在按下"全部"按钮之后的图表上的对象列表

事实上, 图形对象就算您让它不可见, 它在图表上也无法完全隐藏, 这是一个优点而不是缺点. 您可以在列表中快速浏览, 修改或者复制对象的属性, 而不需要检查整个图表, 因为图表中甚至可能包含前些年的对象和柱形. 另外, 列表中的对象可以根据类型, 名称和其他一些参数进行排序.

MetaTrader 5和MetaTrader 4交易终端的众多优点之一就是可以自己简单地开发自动化交易程序, 也可以使用其他人开发的多个应用. 但是人们会永远都不犯错吗?另外, 写程序的准备水平也可能是不一样的. 也有内在的伦理边界. 编程语言就和人一样, 随着时间也能改变和提高.

所以, 如果您能够在程序创建图形时就保证正确, 绝对是件好事. 您可以在列表中快速找到需要的对象并看到它们的属性, 而不是在整个图表上寻找它们. 另外, 您可以确保图表中不含有任何错误或者故意隐藏的对象, 包括程序代码中错误生成的对象.

通过在列表中显示全部对象, 然后选择其中的一些进行删除, 也有可能意外删除控制面板中当时还需要的对象.

在继续程序的回应操作之前, 我们可以利用前文中提到的MQL5和MQL4语言的优点. 我是指能够在代码中使用不同的方法来完成不同的任务.


3. 创建对象的策略

如有必要, 这将有助您在代码中创建您自己的解决方案, 而不是浪费时间在寻找方案中走上错误或者复杂的道路.

为了防止听起来过于正式和专业, 我将告诉您我是怎样找到这种策略的. 之后提到的代码附加在文章的末尾. 为了它们能够运行您需要把代码库中的包含文件ObjectCreateAndSet, 保存到您客户终端的Include文件夹中. 它包含了用于创建对象和通过必要的检查来修改它们属性的函数. 这个文件在附加的test_types_of_chart_events指标的运行中并不需要.


3.1. 在实际写代码之前的考量和操作

根据文档, 您可以使用OnChartEvent()函数, 在图表上有变化或用户事件的时候, 为了您任务的目标, 获得和处理大约9种类型的事件. 它们中的一些是通知关于图形对象的删除, 修改和创建的.

首先, 我是想如何在代码中实现如下功能:

这两种功能都需要同时实现图表上因为某人或者程序重命名的变成"外来者"的对象, 并且不能有更多循环的事件处理.

我的第一个思路是在整个程序中当收到有关对象被修改或者删除的事件时, 来自我检测其属性的变化, 但是这看起来不是最好和最实用的方法. 然后我想到, 完全自我检测可能意味着有许多不必要的操作.

例如, 在对象行为的事件处理中, 图表对象的名称在程序中都会进行比较和跟踪. 当在程序中跟踪任意类型的事件时, 我们的得到的不仅是图形对象的信息, 而是图表上的全部对象. 对象名称的类型是string(字符串), 而 string 类型数据的处理时间比其他类型要长. 图表上可能有很多人工创建或者其他程序创建的对象, 其中可能会进行各种操作, 这样会产生大量的事件.

以下是一个类似的代码框架, 在启用了图表对象删事件通知的情况下, 会进行检测对象名称的匹配比较. 代码中没有进行预先处理以减少比较的次数, 它应用于多个图形对象的处理中:

if(id==CHARTEVENT_OBJECT_DELETE)
        {
         string deletedChartObject=sparam;
         //---
         for(int i=QUANT_OBJ-1;i>=0;i--)
           {
            if(StringCompare(deletedChartObject,nameObj[i],true)==0)
              {
            /*这里有一些代码, 如果
            事件中主题的名称和
            程序中对象的名称完全匹配的话, 会进行相应处理.*/
               return;
              }
           }
        }

另外, "修改"选项 (基于事件或者时间) 不能解决某人或者程序重新命名对象的问题, 并且最终程序还是会使对象失去控制而变得"无主".

这就是为什么我有了使用标记系统的想法. 它和提醒系统类似, 例如信号灯标志等于0, 意思是程序会把对象的改变或者删除认为是没有授权的外部干扰. 在有人或者程序修改删除了对象的时候, 标志的值会改变, 这样删除-修改事件就不再当作是"外部入侵"了.

然而, 因为图表事件会生成队列, 然后在OnChartEvent()函数中进一步处理, 在这个触发修改标志的系统中, 在代码中创建了"自我恢复"的对象时系统并不清楚. 另外, 为了避免循环和重复处理, 在处理删除对象事件时还会产生重复的循环.

例如, 有一个面板只包含几个对象, 而输出信息在处理面板对象被删除的代码中使用了Print()函数. 当通知此事件的时候, 只要几秒钟, 由于对象的自我恢复, 很多注释就会把记录文件塞满, 而同时图表会不断闪烁, 因为对象在不断地重新创建. 而且如果使用以下方法(或者类似的方法)应用了信号灯标志和自我恢复的话, 不论是否按照名称进行选择都会发生:

else  if(id==CHARTEVENT_OBJECT_DELETE)
     {
     if(flagAntivandal==0)
        {
            string deletedChartObject=sparam;
            //---
            for(int i=QUANT_OBJ-1;i>=0;i--)
              {
               //--- 当名称完全匹配:
               if(StringCompare(deletedChartObject,nameObj[i],true)==0)
                 {
                  //--- 在终端的"专家"页面:
                  TEST_PRINT(deletedChartObject);
                  //--- 暂时禁用"提醒", 以便使程序运行更加"友好".
                  flagAntivandal=-1;
                  //--- 删除剩余的对象:
                  ObjectsDeleteAll(0,prefixObj,0,-1);
                  //--- 重新创建面板
                  PanelCreate();
                 //--- 重绘图表 
                  ChartRedraw();
                 //--- 重新启用对象的"保护"标志:
                  flagAntivandal=0;
                  return;
                 }
              }
         return;
        }
      return;
     }//else  if(id==CHARTEVENT_OBJECT_DELETE)

这样写代码会使对象的删除和创建产生循环, 造成一系列的事件通知. 而且, 之后面板上的其它对象被删除后还需要恢复, 如果其他需要绘制的对象(按钮, 输入框等等)被意外或者有意删除, 它们重新创建的时候不能覆盖其他对象.

如果您的计算机没有足够的内存, 那么我不会推荐使用以上的代码. 如果内存足够, 您可以自己测试一下, 在附加的测试代码test_count_click_2中完全替换掉对象删除和修改的事件处理代码.

我没有测试使用错误标志以至于记录文件被填满的最大容量, 因为在引发了错误"自我恢复"事件后, 看到程序的对象不停快速闪烁, 记录不断在"专家"页面增长, 我不得不在2-3秒后就关闭了终端. 我立即删除了巨大增长的记录文件并且清空了回收站.

另外, 当时还是不知道如何解决在代码中重命名对象的问题. 找不到解决这种问题的"着眼点".

这使我下决心重新梳理思路, 再次回想一下在文档中看到的图表事件类型的有关信息, 在查找ObjectSetString()函数的信息时, 它提到在重新命名一个对象之后, 会同时产生两个事件:

在阅读了对象的属性部分之后, 我决定把着眼点放在对象的创建时间上, 它可以使用OBJPROP_CREATETIME属性获得.

例如, 您可以在附加的test_get_objprop_createtime脚本程序中看到, 对象创建的时间等于终端所运行的计算机的本地时间:

对象创建时间等于计算机在创建对象时的本地时间.

图 4. 对象创建时间等于计算机在创建对象时的本地时间.

调用测试脚本程序会在图表上创建一个按钮, 并且取得它的创建时间并调用Print() 函数在专家页面的记录中把它打印出来. 它会在终端的同一页面上设置和打印各种类型的事件: 计算机的本地时间, 交易服务器的估计当前时间, 得到交易服务器最新报价的事件, 格林威治标准(GMT)时间. 然后它会停顿10秒, 再删除它所创建的按钮, 然后终止运行.

下一步就是准备一个行动计划, 在随后创建代码时有更明确的解决方案. 这些最终方案必须足够多功能, 这样在将来就不需要为"每个按钮"都来写单独的恢复操作了. 它们还必须考虑到与图表的和平共处原则.


考虑的行动计划如下:

3.1.1. 检查在以第三方程序作为独立观察工具以及对象被外部影响的程序中, 有哪些事件以及这些时间显示的顺序:

同时也有必要显示创建对象的时间, 把它们写到结果表格中使用起来会更加方便.

从而, 为实现计划第一点我们需要做以下工作:

3.1.2. 一个编程助理指标, 用于外部观察和描述图表上发生的事件.

我选择指标是因为它可以与图表上的EA交易同时运行.

我不会深度讨论这个助理程序, 它的完整代码附在文章附件中, 就是test_types_of_chart_events文件. 但是, 因为在将来当您在处理和熟悉事件的过程中它会有所帮助, 我会在此阐述以下内容:

图 5. 测试观察指标的外部自定义属性

指标含有关于事件的通知, 不与对象的操作直接联系, 默认为禁用. 它所操作的九种标准事件中的五种可以启用/禁用通知.

3.1.3. 参与实验的其他助理程序:


3.2. 进行实验的结果

结果显示在以下表格中. 因为这个程序在查看事件的标准通知方面与其他的程序机制是一样的, 所以我们只要看一个表格就够了. 它是基于"按钮"和"编辑框"对象的操作的.

对象操作 id lparam dparam sparam "人工"操作的事件通知的名称 创建对象的时间 ** 第三方程序"友好"进行某种操作的事件通知
创建对象的时间 **
新对象的创建 (不是重命名) 7
0 0 对象名称 CHARTEVENT_OBJECT_CREATE 运行终端计算机的本地时间 CHARTEVENT_OBJECT_CREATE 运行终端计算机的本地时间
重命名对象(之前名称对象被删除而新对象同时创建)
6

7

8
0

0

0
0

0

0
对象名称,
对象名称,
对象名称
CHARTEVENT_OBJECT_DELETE

CHARTEVENT_OBJECT_CREATE

CHARTEVENT_OBJECT_CHANGE
新对象的创建名称等于之前名称对象的创建时间 没有警告
新对象的创建名称等于之前名称对象的创建时间
修改对象的背景 8 0 0 对象名称 СHARTEVENT_OBJECT_CHANGE 没有变化 没有警告 没有变化
修改对象在X轴的坐标
8 0 0 对象名称 СHARTEVENT_OBJECT_CHANGE 没有变化 没有警告 没有变化
删除对象 6 0 0 对象名称 CHARTEVENT_OBJECT_DELETE *** CHARTEVENT_OBJECT_DELETE ***

 

表格 1. 一个友好的第三方程序"看"到的图表上对象的创建, 修改和删除事件*


注意事项:

* 当程序在处理事件通知时, 其他的通知会形成队列等待程序的处理.

** 我已经注意到, 如果图表上包含的对象在终端重新启动之后没有在程序中重新创建, 终端重启后OBJPROP_CREATETIME函数所返回的对象创建时间等于1970.01.01 00:00:00. 也就是, 前一事件被"重置". 对于图表上人工放置的对象也是如此. 当您在切换时段时, 对象的创建时间不会被重置.

*** 如果您想在对象被删除的通知中使用OBJPROP_CREATETIME取得对象创建时间, 因为对象已经被删除了, 会显示错误信息4203 (错误的图形对象标识符).

如果属性的改变是程序执行的, 就不会显示这样的通知. 与其它的情况一样, 这样做显然是有原因的. 因为一个终端图表上可能运行多个程序, 包括哪些对多个图形对象做多种操作的, 而且可能有多个打开的图表都有这种程序. 所以我猜测程序中对属性改变的事件通知可能会增加对资源的消耗.

为了避免限制用户只使用标准事件通知类型, 开发者可以在代码中创建任意特有事件的通知. EventChartCustom()函数就是用于此目的, 它可以根据指定的自定义事件生成面向个人的或者终端中全部图表的通知.

根据实验结果, 可以提出一些解决方案.

当然它们不能适用于所有情景. 但是, 我相信它们能够满足控制面板的工作, 并且对开发您自己的选项也是有用的.

这些解决方案是基于:

以下是这些方案的共同之处. 不论是否需要对控制面板上的一个还是全部对象进行"保护", 如需实现对象的自我恢复和/或程序从图表上的自我分离, 您需要包含以下代码:

如果程序中的对象被重新命名, 需要重新清理图表, 则需要做到如下几点:

另外, 在代码中实现"安全"操作的顺序是很重要的.

如果您只是构造程序, 在遇到未授权修改/删除对象时, 在代码中实现从图表上"自我分离", 根据这种情况, 一个变量做标志就足够了. 其数值可以是:

if(flagAntivandal==-1){return;}
IndicatorSetString(INDICATOR_SHORTNAME,short_name);
  • ExpertRemovе() 用于EA交易. 如果您现在对此函数还不熟悉, 请查阅文档中的描述和实例.

如果您计划创建一个对象被未经授权的干扰后能够"自我恢复", 至少需要两个标志, 对象会被删除并重新创建. 它们中的一个等于函数中之前的标志. 第二个用来防止代码中产生循环, 因为在对象的恢复操作中可能会收到删除事件. 操作将会比这里稍微复杂一些. 第二个标志是用于自动自我调整的, 它用于保证处理对象删除事件平滑进行. 它会避免不必要的事件处理. 我将会在示例4.2中仔细研究.

现在我们可以继续看例子了.


4. 在未授权修改或删除程序对象时的实现框架

当修改/删除了某些对象后, 在代码中同时实现对象的"自我恢复"和从图表上的"自我分离"实例在本文的第5章中. 它描述了程序运行的代码框架, 如有必要, 可以为您所用.

我们将按照从简单到复杂的步骤, 使用另外的测试代码作为例子, 它会在图表上创建一个面板, 用于计算在其"是"和"否"两个区域中的鼠标点击数量.

这是面板根据交易终端语言设置外观的测试代码

图 6. 面板根据交易终端语言设置外观的测试代码

这是一个简单的测试代码, 用于跟踪运行框架(下文中有描述)的两种类型:

在下文中描述的, 最初的测试指标的代码框架在文章附件中的名称是test_count_click_0. 在实现的过程中, 有两种实现框架, 最终的测试代码也就有了两种变化. 这也是为什么每个框架都包含了完整的代码, 都可以用于测试删除或修改程序对象的操作.

在文章附加文件的代码中, 调用了Print()函数来输出信息, 因而您可以看到框架中当修改或删除对象时的处理顺序. 以下是不包含Print()输出命令的代码行.


4.1. 在删除修改对象图表上程序的"自我分离"

可以运行的完整版测试代码在文章附件中, 名称为test_count_click_1.

4.1.1. 使用此框架的代码文件:

test_count_click_0 文件保存为test_count_click_1. 可以通过在MetaEditor中打开包含编号0的附加文件, 然后在MetaEditor的主菜单中选择文件 -> 另存为... 来实现.

4.1.2. 在所有函数范围之外定义的变量, 在OnInit()函数代码块之前:

指标的名称放在#define中, 可以随后被替换:

#define NAME_INDICATOR             "count_click_1: "

为信号灯标志增加了一个变量:

int flagAntivandal;
之后是一个大小等于"保护的"对象数量的数组, 用于保存创建对象的时间:
datetime timeCreateObj[QUANT_OBJ];

增加了一个文字数组用于保存两个警告信息, 以防指标从图表中"自我分离". 一个是为了成功从图表中删除, 第二个 - 如果操作中出现错误. 声明数组的可见范围是程序的所有部分, 因为这些消息的输出根据终端的语言也被提供了两种语言.

string textDelInd[2];

另外, 此数组中保存的消息可能在别处也有用, 例如在OnInit()函数中因为收到REASON_INITFAILED代码而从图表中被删除.

4.1.3.LanguageTerminal()函数中, 根据交易终端的语言设置显示文本信息:

程序从图表成功"自我分离"或者分离失败的文本信息.

void LanguageTerminal()
  {
   string language_t=NULL;
   language_t=TerminalInfoString(TERMINAL_LANGUAGE);
//---
   if(language_t=="Russian")
     {
      textObj[3]="Да: ";
      textObj[4]="Нет: ";
      textObj[5]="Всего: ";
      //---
      StringConcatenate(textDelInd[0],"Не удалось удалиться индикатору: \"",
                        prefixObj,"\". Код ошибки: ");
      StringConcatenate(textDelInd[1],"Удалился с графика индикатор: \"",
                        prefixObj,"\".");
     }
   else
     {
      textObj[3]="Yes: ";
      textObj[4]="No: ";
      textObj[5]="All: ";
      //---
      StringConcatenate(textDelInd[0],"删除指标失败: \"",
                        prefixObj,"\". 错误编号: ");
      StringConcatenate(textDelInd[1],
                        "从图表指标上撤回: \"",
                        prefixObj,"\".");
     }
//---
   return;
  }

4.1.4. 在创建控制面板的PanelCreate()函数中:

在创建过对象之后, 它们的创建时间就可以确定并保存在数组中. 一个简单版的代码看起来如下:

void PanelCreate(const long chart_ID=0,const int sub_window=0)
  {
   for(int i=NUMBER_ALL;i>=0;i--)
     {
      //--- 显示点击数量的栏位:
      if(ObjectFind(chart_ID,nameObj[i])<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);
         //--- 获取和存储创建对象的时间:
         CreateTimeGet(chart_ID,nameObj[i],timeCreateObj[i]);
        }
     }
//---
   int correct=NUMBER_ALL+1;
//---
   for(int i=QUANT_OBJ-1;i>=correct;i--)
     {
      //--- 注释栏位:
      if(ObjectFind(chart_ID,nameObj[i])<0)
        {
         EditCreate(chart_ID,nameObj[i],sub_window,X_DISTANCE+(WIDTH*2),
                    Y_DISTANCE+(HEIGHT)*(i-correct),WIDTH,HEIGHT,
                    textObj[i],"Arial",FONT_SIZE,"\n",ALIGN_LEFT,true,
                    CORNER_PANEL,clrText[i-correct],CLR_PANEL,CLR_BORDER);
         //--- 获取和存储创建对象的时间:
         CreateTimeGet(chart_ID,nameObj[i],timeCreateObj[i]);
        }
     }
   return;
  }

4.1.5. 用于把对象创建时间存储到变量中的函数:

bool CreateTimeGet(const long chart_ID,
                   const string &name,
                   datetime &value
                   )
  {
   datetime res=0;
   if(!ObjectGetInteger(chart_ID,name,OBJPROP_CREATETIME,0,res))
     {
      Print(LINE_NUMBER,__FUNCTION__,", 错误 = ",
            GetLastError(),", 名称: ",name);
      return(false);
     }
   value=res;
   return(true);
  }

如果程序在图表上正好遇到终端紧急重启, 例如终端程序锁死, 增加了一个修改对象创建时间的函数:

bool RevisionCreateTime(int quant,datetime &time_create[])
  {
   datetime t=0;
   for(int i=quant-1;i>=0;i--)
     {
      t=time_create[i];
      if(t==0)
        {
         Print(LINE_NUMBER,__FUNCTION__,", 错误创建时间: ",
               TimeToString(t,TIME_DATE|TIME_SECONDS));
         return(false);
        }
     }
//---
   return(true);
  }

调用此函数的位置在OnInit()函数中, 在PanelCreate()函数之后.

我假定在终端紧急关闭后再重新启动的时候, 函数返回false, 在此之前, CreateTimeGet()函数会显示错误信息4102, 说明图表没有回应, 信息出现在终端的"专家"页面.

4.1.6. OnInit()函数区块中:

4.1.6.1. 在创建控制面板的函数之前, 增加的信号灯标志使用-1值进行初始化. 意思是对于对象执行"友好"操作的时候禁止提醒. 特别是在图表时段发生改变是避免干扰.

flagAntivandal=-1;

4.1.6.2. 如果尚未设好, 在此设置指标的短名称. 这在程序被从图表上强行删除时有用.

在提供的测试代码示例中, 指标的短名称之前已经设置好, 等于对象的前缀, 包括指标名称, 交易品种和图表的时段:

//--- 创建代码中对象名称的前缀和指标的短名称:
   StringConcatenate(prefixObj,NAME_INDICATOR,Symbol(),"_",EnumToString(Period()));
//--- 设置指标的名称:
   IndicatorSetString(INDICATOR_SHORTNAME,prefixObj);

4.1.6.3. 在函数创建了控制面板和修改对象创建时间的数组之后, 我们启用"保护"提醒 :

//--- 创建面板:
   PanelCreate();
/*修改创建对象时间的数组, 
如果出错则程序退出:*/
   if(!RevisionCreateTime(QUANT_OBJ,timeCreateObj))
     {return(REASON_INITFAILED);}
/*我们设置回应标志
以对应对象的删除和修改事件:*/
   flagAntivandal=0;


4.1.6.4. 如果被禁用, 我们启用图表中图形对象修改删除事件的通知:

//--- 启用图形对象删除事件的通知
 bool res=true;
 ChartEventObjectDeleteGet(res);
 if(res!=true)
 {ChartEventObjectDeleteSet(true);}


Note: ChartEventObjectDeleteGet()ChartEventObjectDeleteSet() 是写好的函数 , 它们位于图表操作文档的实例程序中.

4.1.6.5. 在退出OnInit()函数之前, 我们设置计时器的秒数频率, 用于检查在程序的运行中, 从图表上删除对象事件的通知是否被禁用:

EventSetTimer(5);

4.1.7. OnDeinit() 函数块中:

我们指定禁止"提醒":

flagAntivandal=-1;

我们设置终止计时器生成事件:

EventKillTimer();

如果来自OnInit()的终止原因代码是REASON_INITFAILED, 就删除指标:

//--- 如果 OnInit() 传来 REASON_INITFAILED, 删除指标.
   if(reason==REASON_INITFAILED)
     {
      ChIndicatorDelete(prefixObj,textDelInd[0],textDelInd[1],0,0);
     }

4.1.8. OnTimer()函数区块:

使用复制和粘贴, 从4.1.6.4节部分复制代码行, 以在计时器函数中启用删除对象事件的通知, 以防它们被其他程序禁止.

4.1.9.OnChartEvent()函数中进行操作:

4.1.9.1. 如果只是为了"安全"的目的来处理对象的删除和修改时间, 这些事件的处理可以写在一个代码块中.

4.1.9.2. 第一件事是检查标志, 如果在收到关于修改/删除对象的通知时启用了"提醒". 如果标志值等于 -1 ("提醒"被禁用), 推出事件处理模块.

4.1.9.3. 当标志的值等于0 ("提醒"被启用), 您可以首先检查在通知中对象的名称是否与程序对象的前缀所匹配. 为此需要使用StringFind()函数, 而不需要在每个图表对象被修改或删除的通知处理中都遍历整个程序对象的名称这种情况下, 如果OnInit()函数中设置的前缀不是很短, 过滤效果会更好. 前缀的构造在p.4.1.6.2中提供.

如果没有匹配的前缀, 则退出事件处理模块.

4.1.9.4. 如果前缀有匹配, 再根据事件通知和程序中的"被保护"对象做名称的完全匹配. 当名称能够完全匹配, 保护标志改为-1 (禁止), 以避免程序在从图表上删除时出现额外的循环.

4.1.9.5. 如果有对象因为被重新命名而变成"无主"状态, 就搜索在通知运行时有没有相同创建事件的对象, 如果找到将会把它删除.

4.1.9.6. 直到此时才能从图表上删除程序.

代码中操作的描述:

else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
     {
      if(flagAntivandal==-1)//如果提醒被禁止, 则忽略事件
        {return;}
      else if(flagAntivandal==0)//如果提醒被启用,
        {
         //--- 寻找前缀的匹配:
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)//如果前缀匹配
           {
            string chartObject=sparam;
            findPrefixObject=-1;
            //---
            for(int i=QUANT_OBJ-1;i>=0;i--)
              {
               //--- 当名称完全匹配:
               if(StringCompare(chartObject,nameObj[i],true)==0)
                 {
                  //--- 禁止提醒以避免循环删除操作:
                  flagAntivandal=-1;
/*删除创建时间等于修改或者删除对象的创建时间
 的对象:*/
                  ObDelCreateTimeExcept0(timeCreateObj[i],0,0,OBJ_EDIT);
                  //--- 从图表上删除指标:
                  ChIndicatorDelete(prefixObj,textDelInd[0],textDelInd[1],0,0);
                  //---
                  ChartRedraw();
                  return;
                 }
              }//for(int i=QUANT_OBJ-1;i>=0;i--)
            return;
           }//if(findPrefixObject==0)
         return;
        }//if(flagAntivandal==0)
      return;
     }//if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)  

4.1.10. ObDelCreateTimeExcept0()函数根据它们的创建事件删除对象:

根据创建时间来删除对象的函数框架看起来如下:

void ObDelCreateTimeExcept0(datetime &create_time,// 创建时间
                            long chart_ID=0,// 图表id
                            int sub_window=-1,// 子窗口编号
                            ENUM_OBJECT type=-1)// -1 = 全部类型的对象
  {
/*从函数退出, 如果对象时间 = 0(1970.01.01 00:00:00),
 所以不需要从图表上删除人工或者程序创建的对象, 
 它们的创建时间 
 在终端重新启动的时候变成0了*/
   if(create_time==0){return;}
   int quant_obj=0;
   quant_obj=ObjectsTotal(chart_ID,sub_window,type);
//---
   if(quant_obj>0)
     {
      datetime create_time_x=0;
      string obj_name=NULL;
      //---
      for(int i=quant_obj-1;i>=0;i--)
        {
         obj_name=ObjectName(chart_ID,i,sub_window,type);
         create_time_x=(datetime)ObjectGetInteger(chart_ID,obj_name,
                        OBJPROP_CREATETIME);
         //---
         if(create_time_x==create_time)
           {ObDelete(chart_ID,obj_name);}
        }
     }
   return;
  }
//+------------------------------------------------------------------+


它的内部有个过滤器, 如果"被保护的"对象的创建时间等于0 (意思是: 1970.01.01 00:00:00), 就不会继续处理事件. 这是要求避免在图表上删除终端重新启动之后, 创建时间被清零的人工或者程序创建的对象.

4.1.11. 以上代码中的ObDelete()函数:

它是来自ObjectCreateAndSet函数库的, 在删除对象出错时会输出错误:

bool ObDelete(long chart_ID,string name)
  {
   if(ObjectFind(chart_ID,name)>-1)
     {
      ResetLastError();
      if(!ObjectDelete(chart_ID,name))
        {
         Print(LINE_NUMBER,__FUNCTION__,", 错误代码 = ",
               GetLastError(),", 名称: ",name);
         return(false);
        }
     }
   return(true);
  }

4.1.12. 删除指标的函数:

//+------------------------------------------------------------------+
//| 从图表上删除一个指标                             |
//+------------------------------------------------------------------+
void ChIndicatorDelete(const string short_name,//指标的短名称
                       const string &text_0,//当删除指标出错时的文本信息
                       const string &text_1,//当删除指标成功时的文本信息
                       const long  chart_id=0,// 图表id
                       const int   sub_window=-1// 子窗口编号
                       )
  {
   bool res=ChartIndicatorDelete(chart_id,sub_window,short_name);
//---
   if(!res){Print(LINE_NUMBER,text_0,GetLastError());}
   else Print(LINE_NUMBER,text_1);
  }
//+------------------------------------------------------------------+

其中LINE_NUMBER是在信息之前的通用文本, 它包含了代码行号, 在#define中设置:

#define LINE_NUMBER    "Line: ",__LINE__,", "

现在我们看下面的框架.


4.2. 程序对象的"自我恢复"

构造对象的自我恢复更为复杂, 需要特别注意标志数值的改变以避免程序运行时产生循环.

此代码框架是基于之前的test_count_click_1 代码修改的. 完整的代码可供查阅和测试, 您可以在附加的名为test_count_click_2文件中找到它们.

4.2.1.使用代码框架构造文件:

test_count_click_1 文件以 test_count_click_2名称另存.

4.2.2. 在所有函数范围之外定义的变量, 即位于OnInit() 函数块之外:

指标的名称在#define之后, 未来可以方便替换:

#define NAME_INDICATOR             "count_click_2: "

另外增加了一个变量, 除了前面描述的主标志, 这一次是辅助适应性标志:

int flagAntivandal, flagResetEventDelete;

在进行恢复操作之前也创建一个用于信息文字的变量:

string textRestoring;

4.2.3. LanguageTerminal()函数用于根据交易终端语言显示消息的文本:

它包含了如果未经授权删除或者修改了控制面板对象进行恢复操作之前的信息:

void LanguageTerminal()
  {
   string language_t=NULL;
   language_t=TerminalInfoString(TERMINAL_LANGUAGE);
//---
   if(language_t=="Russian")
     {
      textObj[3]="Да: ";
      textObj[4]="Нет: ";
      textObj[5]="Всего: ";
      //---
      textRestoring="Уведомление о страховом случае: объект ";
      //---
      StringConcatenate(textDelInd[0],"Не удалось удалиться индикатору: \"",
                        prefixObj,"\". Код ошибки: ");
      StringConcatenate(textDelInd[1],"Удалился с графика индикатор: \"",
                        prefixObj,"\".");
     }
   else
     {
      textObj[3]="Yes: ";
      textObj[4]="No: ";
      textObj[5]="All: ";
      //---
      textRestoring="确认时间的通知: 对象 ";
      //---
      StringConcatenate(textDelInd[0],"删除指标失败: \"",
                        prefixObj,"\". 错误编号: ");
      StringConcatenate(textDelInd[1],
                        "从图表指标上撤回: \"",
                        prefixObj,"\".");
     }
//---
   return;
  }

我使用这里的文字来确保遇到意外事件后的操作已经安全完成.

4.2.4. OnInit() 函数代码块中:

在创建对象之前我们把两个标志初始值都设为 -1 (禁用提醒):

flagAntivandal = flagResetEventDelete = -1;

在创建面板对象和修改创建对象的时间之后 - 把标志值初始化为0 (启用"提醒"):

//--- 创建面板:
   PanelCreate();
/*修改创建对象时间的数组, 
如果出错则程序退出:*/
   if(!RevisionCreateTime(QUANT_OBJ,timeCreateObj))
     {return(REASON_INITFAILED);}
/*设置反恶意破坏标志, 位置在 
对删除或者修改对象进行回应的地方:*/
   flagAntivandal=flagResetEventDelete=0;

4.2.5. 让我们继续OnChartEvent() 函数:

我们将会按照下面的方式修改之前修改和删除对象的处理事件通知的代码块. 在新的代码块中, 处理删除和修改对象事件的框架是通用的:

  • 首先, 检查是否启用了"保护". 如果没有, 则退出事件处理模块.
  • 然后, 如果用于重设多余删除的辅助标志大于0, 删除和修改面板对象通知会被重置, 直到标志等于0.
  • 只有当主标志和辅助标志都等于0的时候, 遇到未经授权的修改或删除对象, 才会进行恢复操作.
else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
     {
      if(flagAntivandal==-1){return;}
      else if(flagResetEventDelete>0)
        {
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)
           {
            findPrefixObject=-1;
/*重置事件计数器标志以避免
在删除对象时事件形成队列造成循环
以至于恢复工作错误:*/
            flagResetEventDelete=flagResetEventDelete-1;
            //---
            return;
           }
         return;
        }
      else if(flagAntivandal==0 && flagResetEventDelete==0)
        {
         //--- 根据前缀检查匹配:
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)//如果前缀匹配
           {
            string chartObject=sparam;//事件中的对象名称
            findPrefixObject=-1;
/*检查对象是否"被保护", 
            如果是, 就进行恢复工作:*/
            RestoringObjArrayAll(chartObject,prefixObj,flagAntivandal,
                                     flagResetEventDelete,QUANT_OBJ,nameObj,
                                     timeCreateObj);
            return;
           }//if(findPrefixObject==0)
         return;
        }//else if(flagAntivandal==0 && flagResetEventDelete==0)
      return;
     }//else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)

主要恢复工作创建于RestoringObjArrayAll()函数中. 几个类似的函数可以同时应用于不同组别的对象. 您可以在本文的第五章找到相关例子. 在本函数中, 会根据删除或修改时间中对象的名称来检查前缀, 看它是否与"被保护"对象匹配.

4.2.6. RestoringObjArrayAll() 函数.

它包含了以下操作:

  • 如果根据名称检查发现事件相关对象是"被保护的", 则:
    • 把 "提醒" 主标志的值设为-1 (在"友好"操作期间禁止"提醒");
    • 如果是删除对象事件, 把一个辅助标志设为"被保护"对象总数减1, 因为是在处理删除对象事件, 意思是当时至少有一个对象被删除了.
  • 在这些预先操作之后, 剩余的被保护对象根据前缀使用以下的ObDeletePrefixFlag()函数来删除, 同时计算被删除的对象数量. 基于此计算出flagResetEventDelete辅助标志的值, 如果有必要则进行修改.

因为删除对象的时候还是会出现删除事件, 它们将不会被处理, 知道辅助标志的值重置到0, 才会继续在代码中处理事件:

else if(flagResetEventDelete>0)
        {
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)
           {
            findPrefixObject=-1;
/*重设事件计数器标志值, 以避免 
在删除对象时事件形成队列造成循环
以至于恢复工作错误:*/
            flagResetEventDelete=flagResetEventDelete-1;
            //---
            return;
           }
         return;
        }


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

注意: 函数中没有对象类型的选择, 这是有原因的. 因为在对象的"自我恢复"中, 为了减少事件的处理, 减少循环而使用对象类型的判断, 在需要恢复面板上的全部对象的时候, 可能会适得其反. 所以, 就我看来, 最好还是不要根据对象的类型来做特别处理以减少循环的处理, 因为对图表对象的故意破坏可能是针对各种类型对象的. 我想可能会有人忽略此警告, 以上函数就删除了这个选项.

如果您在使用本文第五章的例子时, 区分对象类型来做"自我恢复", 面板上如果有多种类型的对象, 您可能得不到期望的结果. 您只需要在处理事件和恢复的代码中的加入Print()函数.

我应该加一条, 在从"自我分离"的方案中, 您可以通过限制对象类型来减少代码中的处理以节约资源.

4.2.7. ObDeletePrefixFlag() 函数是用于根据前缀删除对象, 如有必要, 再修正辅助标志的数值:

我利用了Sergei Kovalev书中的代码框架, 它后来在MQL4 论坛中, Artem Trishkin 和我所不认识的用户 7777877都提到过. 我当时没有注意, 所以现在对他们非常感谢, 因为这个框架很通用, 在很多任务中帮了我的大忙.

代码基本如下:

//+------------------------------------------------------------------+
//| int &flag_reset = 重置删除对象事件的标志  |
//| string prefix_obj = 对象名称的通用前缀         |
//| long  chart_ID = 图表id                              | 
//| int   sub_window=-1 = 窗口索引 (-1 = 全部)                  |
//| int start_pos=-1 = 通用前缀的起始位置          |
//| 对象名称中的substring 位置                                   |
//+------------------------------------------------------------------+
int ObDeletePrefixFlag(int &flag_reset,
                       string prefix_obj,
                       const long chart_ID=0,
                       const int sub_window=0,
                       int start_pos=0)

  {
   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);
         find_substr=StringSubstr(obj_name,start_pos,prefix_len);

         if(StringCompare(find_substr,prefix_obj,true)==0)
           {
            count=count+1;
            ObDelete(chart_ID,obj_name);
           }
        }
      if(count>0 && flag_reset<count)
        {flag_reset=count;}
     }
   return(flag_reset);
  }

可以如下检查代码的运行:
  • 在图表上运行测试代码;
  • 点击属于"是" 和 "否"区域的对象, 使得数值从0增加到其他数值;
  • 然后尝试通过属性对话框修改面板对象或者删除它们.
在执行过自动回复操作后, 面板上还是含有"已经点击"的数量, 因为点击数量已经保存在数据数组中, 它们将用于重新创建面板.

然而, 针对不同对象可以给予不同的回复.

我们已经在主要部分演示了它们, 对于特殊状况, 例如:

现在, 让我们继续在实际例子中实现这两种回应.


5. 在一个程序内实现两个恢复选项的实例

选择一个指标, 面对未经授权的操作, 在其中实现两种回应, 按步骤进行而不是随机:

如果未来有必要, 可以扩展列表显示数值.

附加的完整代码名称为id_name_object. 在此小结一下:

提供指标的目的: 当点击任何图表对象时, 它显示被点击对象的名称, 它的创建事件和类型. 如有必要, 您可以从这些栏位中复制这些数值. 在尝试修改或者删除这些显示的数值时, 它们会自我恢复. 这正是本文提到的功能选项, 而已经被加入其中.

另外, 还有以下的功能:

控制面板的外观依赖于终端的语言

图 7. 控制面板的外观依赖于终端的语言

用于保存面板对象创建名称的变量和数组的映射

图 8. 用于保存面板对象创建名称的变量和数组的映射


名称
对象类型
使用目的
文字保存位置
字体
文字颜色*
背景色* 边框颜色 *
创建时间
nameRectLabel
OBJ_RECTANGLE_LABEL
画布
---
--- ---
CLR_PANEL clrNONE
timeCreateRectLabel
nameButton[0]
OBJ_BUTTON
用于从图表上删除对象的按钮
textButtUchar[0]**
"Wingdings"
CLR_TEXT
CLR_PANEL clrNONE
timeCreateButton[0]
nameButton[1] OBJ_BUTTON
"最小化"面板的按钮
textButtUchar[1]**
"Wingdings"
CLR_TEXT
CLR_PANEL clrNONE
timeCreateButton[1]
nameButton[2] OBJ_BUTTON
在"最小化"面板后的"最大化"按钮
textButtUchar[2]**
"Wingdings"
CLR_TEXT
CLR_PANEL clrNONE
timeCreateButton[2]
nameEdit0[0]
OBJ_EDIT
"对象名称:" 标签
textEdit0[0]
"Arial"
CLR_TEXT_BACK
CLR_PANEL CLR_BORDER
timeCreateEdit0[0]
nameEdit0[1]
OBJ_EDIT
"创建时间:" 标签 textEdit0[1] "Arial"
CLR_TEXT_BACK
CLR_PANEL CLR_BORDER
timeCreateEdit0[1]
nameEdit0[2]
OBJ_EDIT
"对象类型:" 标签 textEdit0[2] "Arial"
CLR_TEXT_BACK
CLR_PANEL CLR_BORDER
timeCreateEdit0[2]
nameEdit1[0]
OBJ_EDIT
显示对象名称的栏位 textEdit1[0]
"Arial"
CLR_TEXT
CLR_PANEL CLR_BORDER
timeCreateEdit1[0]
nameEdit1[1] OBJ_EDIT
显示创建事件的栏位 textEdit1[1] "Arial"
CLR_TEXT
CLR_PANEL CLR_BORDER
timeCreateEdit1[1]
nameEdit1[2] OBJ_EDIT
显示对象类型的栏位
textEdit1[2] "Arial"
CLR_TEXT
CLR_PANEL CLR_BORDER
timeCreateEdit1[2]

 

Table 2. 面板对象地图

* 定义颜色#define:

#define CLR_PANEL                  clrSilver//面板背景色
#define CLR_TEXT                   C'39,39,39'//主文字色
#define CLR_TEXT_BACK              C'150,150,150' //影子文字颜色 
#define CLR_BORDER                 clrLavender//边框颜色

** Symbols "Wingdings":

uchar textButtUchar[3]={251,225,226};


5.1. 外部变量:

面板的大小是基于按钮的大小来计算的. 因而, 按钮的宽度和高度以及字体大小显示在外部自定义属性中. 这使我们可以不用修改代码就增加面板和显示文字的大小.

指标的外部自定义属性

图 9. 指标的外部自定义属性


5.2. 标志变量的声明是在任何函数之外的, 所以在程序的任何地方都是可见的, 也就是在OnInit()函数块之前:

声明了两个标志:

int flagClick, flagResetEventDelete;

flagClick适用于设置跟踪面板按钮的点击通知的标志. 它的数值为:

0
跟踪点击面板"最小化"按钮的通知事件, 并且当面板的主要部分显示在图表上时, 从图表上删除按钮.
-1
当图表上的面板进行"最小化"或者相反的"最大化"之前设置, 同时也是在图表上删除对应按钮之前设置.
1 跟踪点击面板"最大化"按钮的通知事件.

 

另外, 当有未经授权的删除或者修改对象时, 它还作为主要的"反破坏"标志(在本文的早些时候, 它的名字是flagAntivandal). 0 启用面板"提醒"的主要部分, 1 — "最大化"按钮, 以及-1禁用操作时的"提醒".

如果您想创建一个灵活适应的主"反破坏"标志, 使用一个辅助标志在代码中重设事件的处理, 那么则需要为此增加一个单独的变量.

flagResetEventDelete 是一个在我们实现修改或者删除对象的"自我恢复"时, 用于重置不需处理事件的标志.

本区域中其它变量的声明可参见附加的代码.


5.3. OnInit()函数区块中:

int OnInit()
  {
   flagClick=flagResetEventDelete=-1;
/*面板文字数组 
   用于显示点击的图形对象属性内容:*/
   for(int i=QUANT_OBJ_EDIT-1;i>=0;i--){textEdit1[i]=" ";}
//+------------------------------------------------------------------+
/*指标的短名称:*/
   StringConcatenate(prefixObj,INDICATOR_NAME,"_",Symbol(),"_",
                     EnumToString(Period()));
   IndicatorSetString(INDICATOR_SHORTNAME,prefixObj);
//--- 创建对象的名称
   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");
//+------------------------------------------------------------------+
/*指标的文字, 依赖于交易终端的语言:*/
   LanguageTerminal();
//+------------------------------------------------------------------+
/*限制按钮的宽度和高度:*/
   if(objWidthHeight>=20 && objWidthHeight<=300)
     {obj0WidthHeight=objWidthHeight;}
   else if(objWidthHeight>300){obj0WidthHeight=300;}
   else {obj0WidthHeight=20;}
//--- 在图表上创建控制面板
   PanelCreate();
/*用于保存和修改创建对象时间的数组,
如果出错则退出程序:
(面板的主体部分三个按钮只会显示两个,
所以在有变化的时候它们的数量会减1)*/
   if(!RevisionCreateTime(QUANT_OBJ_BUTTON-1,timeCreateButton))
     {return(REASON_INITFAILED);}
   if(!RevisionCreateTime(QUANT_OBJ_EDIT,timeCreateEdit0))
     {return(REASON_INITFAILED);}
   if(!RevisionCreateTime(QUANT_OBJ_EDIT,timeCreateEdit1))
     {return(REASON_INITFAILED);}
//---
   flagClick=flagResetEventDelete=0;
//--- 包含删除图形对象事件的通知:
   bool res;
   ChartEventObjectDeleteGet(res);
   if(res!=true)
     {ChartEventObjectDeleteSet(true);}
//--- 创建计时器
   EventSetTimer(8);
//---
   return(INIT_SUCCEEDED);
  }

5.4.PanelCreate() 函数中:

面板上的对象按名称分成两组, 以便将来修改外观时更加简单. 面板本身放在图表的右上角. 相应地, X轴和Y轴上的像素点数距离是相对于图表左上角的.

如果对象被未经授权修改了名称, 它们的创建时间会被设置和记录在数组中. 面板的画布使用了单独的变量.

void PanelCreate(const long chart_ID=0,//chart's ID 
                 const int sub_window=0)// subwindow's number
  {
//--- 面板画布的宽度:
   int widthPanel=(obj0WidthHeight*11)+(CONTROLS_GAP_XY*2);
//--- 面板画布的高度:
   int heightPanel=(obj0WidthHeight*6)+(CONTROLS_GAP_XY*8);
//--- x轴上的距离:
   int x_dist=X_DISTANCE+widthPanel;
//---
   if(ObjectFind(chart_ID,nameRectLabel)<0)//canvas
     {
      RectLabelCreate(chart_ID,nameRectLabel,sub_window,x_dist,
                      Y_DISTANCE,widthPanel,heightPanel,"\n",CLR_PANEL,
                      BORDER_RAISED,CORNER_PANEL,clrNONE,STYLE_SOLID,1,
                      false,false,true);
      //--- 取得并记录创建对象的时间:
      CreateTimeGet(chart_ID,nameRectLabel,timeCreateRectLabel);
     }
//---
   x_dist=X_DISTANCE+CONTROLS_GAP_XY;
   int y_dist=Y_DISTANCE+CONTROLS_GAP_XY;
//---
   for(int i=QUANT_OBJ_BUTTON-2;i>=0;i--)
     {
      //--- 用于删除和最小化指标的按钮:
      if(ObjectFind(chart_ID,nameButton[i])<0)
        {
         ButtonCreate(chart_ID,nameButton[i],sub_window,
                      x_dist+(obj0WidthHeight*(i+1)),y_dist,obj0WidthHeight,
                      obj0WidthHeight,CORNER_PANEL,
                      CharToString(textButtUchar[i]),"\n","Wingdings",
                      font_sz+1,CLR_TEXT,CLR_PANEL,clrNONE);
         //--- 取得并记录创建对象的时间:
         CreateTimeGet(chart_ID,nameButton[i],timeCreateButton[i]);
        }
     }
//---
   x_dist=X_DISTANCE+widthPanel-CONTROLS_GAP_XY;
   int y_dist_plus=(CONTROLS_GAP_XY*2)+(obj0WidthHeight*2);
//---
   for(int i=QUANT_OBJ_EDIT-1;i>=0;i--)
     {
      //--- 对象属性的名称:
      if(ObjectFind(chart_ID,nameEdit0[i])<0)
        {
         EditCreate(chart_ID,nameEdit0[i],sub_window,x_dist,
                    y_dist+(y_dist_plus*i),obj0WidthHeight*8,
                    obj0WidthHeight,textEdit0[i],"Arial",font_sz,
                    "\n",ALIGN_CENTER,true,CORNER_RIGHT_UPPER,
                    CLR_TEXT_BACK,CLR_PANEL,CLR_BORDER);
         //--- 取得并记录创建对象的时间:
         CreateTimeGet(chart_ID,nameEdit0[i],timeCreateEdit0[i]);
        }
     }
//---
   y_dist=Y_DISTANCE+obj0WidthHeight+(CONTROLS_GAP_XY*2);
//---
   for(int i=QUANT_OBJ_EDIT-1;i>=0;i--)
     {
      //--- 用于显示对象属性值的文字:
      if(ObjectFind(chart_ID,nameEdit1[i])<0)
        {
         EditCreate(chart_ID,nameEdit1[i],sub_window,x_dist,
                    y_dist+(y_dist_plus*i),obj0WidthHeight*11,
                    obj0WidthHeight,textEdit1[i],"Arial",font_sz,
                    "\n",ALIGN_LEFT,true,CORNER_RIGHT_UPPER,
                    CLR_TEXT,CLR_PANEL,CLR_BORDER);
         //--- 取得并记录创建对象的时间:
         CreateTimeGet(chart_ID,nameEdit1[i],timeCreateEdit1[i]);
        }
     }
   return;
  }

5.5. OnDeinit()函数中:

在从图表删除指标期间"禁用"主标志, 根据前缀删除对象, 在计时器中停止生成事件并从图表中删除指标, 如果从OnInit()函数中收到的终止原因是REASON_INITFAILED:

void OnDeinit(const int reason)
  {
   flagClick=-1;
   ObjectsDeleteAll(0,prefixObj,0,-1);
   EventKillTimer();
//--- 如果在OnInit函数中收到 REASON_INITFAILED, 就删除指标.
   if(reason==REASON_INITFAILED)
     {
      ChIndicatorDelete(prefixObj,textDelInd[0],textDelInd[1],0,0);
     }
//---
   ChartRedraw();
  }

5.6.OnTimer()函数中:

OnInit()函数中根据时间检查状态, 如果图表的属性中包含了删除图形对象事件的通知:

void OnTimer()
  {
//--- 包含关于删除图形对象事件的通知
   bool res;
   ChartEventObjectDeleteGet(res);
   if(res!=true)
     {ChartEventObjectDeleteSet(true);}
  }


5.7. OnChartEvent()函数:

5.7.1. 在处理使用鼠标点击对象事件的通知处理代码块中:
如果flagClick 信号灯标志等于0, 那么:
  • 首先进行检查, 来看是否在控制面板主要部分的按钮上有点击.
  • 如果点击在面板的最小化按钮上, 那么flagClick 标志在其他操作之前把值设为-1.
  • 如果点击了"最小化"按钮, 控制面板的主要部分就最小化, 并且在图表右上角创建一个"最大化"按钮.
  • 之后flagClick标志设为1, 表明开始追踪面板上"最大化"按钮的点击以及未经授权的修改或者删除.
  • 如果按钮上有点击, 用于从图表上删除指标, 则在其他更多操作之前把flagClick标志也设为-1. 然后再尝试从图表上删除指标.
  • 如果控制面板的按钮上没有点击, 而是点击了图表上除了指标对象之外的其他对象, 那么它的名称, 创建时间和类型就会显示在此对象的数值面板上.

如果flagClick 信号灯标志等于1, 则通过点击"最大化"按钮:

  • 在其他操作之前把标志值设为-1.
  • 删除此按钮并创建面板.
  • flagClick标志设为 0之后. 辅助用于重设删除"最大化"按钮事件的flagResetEventDelete标志数值设为1.
if(id==CHARTEVENT_OBJECT_CLICK)//id = 1
     {
      if(flagClick==0)//如果控制面板主要部分在图表上
        {
         string clickedChartObject=sparam;
         findPrefixObject=StringFind(clickedChartObject,prefixObj);
         //---
         if(findPrefixObject==0)
           {
            findPrefixObject=-1;
            //---
            for(int i=1;i>=0;i--)
              {
               if(StringCompare(clickedChartObject,nameButton[i],true)==0)
                 {
                  switch(i)
                    {
                     //--- "最小化" 按钮:
                     case 1:flagClick=-1;
                     MinimizeTable(nameButton,2,textButtUchar,2,
                                   timeCreateButton,obj0WidthHeight);
                     flagClick=1;return;
                     //--- 用于从图表上删除指标的按钮:
                     case 0:flagClick=-1;
                     ChIndicatorDelete(prefixObj,textDelInd[0],textDelInd[1],0,0);
                     return;
                     default:return;
                    }
                 }
              }
           }
         else//如果没有点击在控制面板的按钮上,
           {
            SetValuesTable(clickedChartObject);//我们设置对象的值
            return;
           }
        }
      //--- 如果点击了 "最大化" 按钮:
      else if(flagClick==1)
        {
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)
           {
            string clickedChartObject=sparam;
            findPrefixObject=-1;
            //--- "最大化"面板按钮:
            if(StringCompare(clickedChartObject,nameButton[2],true)==0)
              {
               flagClick=-1;
               //--- 当按下按钮"最大化"面板的操作
               ToExpandTheTable(clickedChartObject);
               //---
               flagClick=0;
               //--- 重设删除此按钮的事件:
               flagResetEventDelete=1;
               //---
               return;
              }
           }
         return;
        }
      return;
     }//if(id==CHARTEVENT_OBJECT_CLICK)

5.7.2. 在处理编辑框图形对象的结束文字编辑通知的代码块中:
当面板上的这些栏位有数值之后, 就可以从这些栏位复制数据了. 因而, 如果指标在复制的时候显示的数值被有意无意修改或者删除了, 之前的文字会恢复:
else  if(id==CHARTEVENT_OBJECT_ENDEDIT)//id = 3
     {
      findPrefixObject=StringFind(sparam,prefixObj);
      if(findPrefixObject==0)
        {
         string endEditChartObject=sparam;
         findPrefixObject=-1;
         int comparison=-1;
         //---
         for(int i=QUANT_OBJ_EDIT-1;i>=0;i--)
           {
            if(StringCompare(endEditChartObject,nameEdit1[i],true)==0)
              {
               string textNew=ObjectGetString(0,endEditChartObject,OBJPROP_TEXT);
               comparison=StringCompare(textEdit1[i],textNew,true);
               //---
               if(comparison!=0)
                 {
                  ObSetString(0,nameEdit1[i],OBJPROP_TEXT,textEdit1[i]);
                 }
               //---
               ChartRedraw();
               return;
              }
           }//for(int i=0;i<QUANT_OBJ_EDIT;i++)
         return;
        }//if(findPrefixObject==0)
      return;
     }//if(id==CHARTEVENT_OBJECT_ENDEDIT)

5.7.3. 在处理删除或修改对象通知的代码块中:
  • 如果flagClick 标志等于-1, 就从事件处理区块推出, 因为它的数值反应了"友好"的操作.
  • 如果第一次检查完成了, 并且发现用于重设事件的flagResetEventDelete辅助标志超过了0, 而且发生事件的对象属于面板对象, 然后事件就会被重设并从处理代码块退出.
  • 如果 flagClickflagResetEventDelete标志等于0, 那么, 当发生事件对象的名称与"被保护"对象名称匹配, 就会面板主要部分的恢复工作, 使用的是我们已经知道的RestoringObjArrayAll() 和其他相关函数.
  • 如果flagClick主标志在事件发生时等于1, 那么当事件中的对象名称与面板上"最大化"按钮名称匹配时, 指标会从图表上删除.

else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)
     {
      if(flagClick==-1)return;
      else if(flagResetEventDelete>0)
        {
         findPrefixObject=StringFind(sparam,prefixObj);
         if(findPrefixObject==0)
           {
            findPrefixObject=-1;
            flagResetEventDelete=flagResetEventDelete-1;
            //---
            return;
           }
         return;
        }
      //--- 检查我们的对象是否被删除:
      else if(flagClick==0 && flagResetEventDelete==0)
        {
         findPrefixObject=StringFind(sparam,prefixObj);
         //---
         if(findPrefixObject==0)
           {
            string chartObject=sparam;
            findPrefixObject=-1;
            int res=-1;
            //--- 如果对象在控制面板上绘制的操作:
            res=RestoringObjOneAll(chartObject,prefixObj,flagClick,
                                   flagResetEventDelete,nameRectLabel,
                                   timeCreateRectLabel,QUANT_OBJ_ALL);
            //---
            if(res==1){return;}
            //--- 如果对象是控制面板上按钮的操作:
            res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                                     flagResetEventDelete,QUANT_OBJ_BUTTON,
                                     nameButton,timeCreateButton,QUANT_OBJ_ALL);
            //---
            if(res==1){return;}
            //--- 如果对象是控制面板注释栏位(Edit0)的操作:
            res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                                     flagResetEventDelete,QUANT_OBJ_EDIT,
                                     nameEdit0,timeCreateEdit0,QUANT_OBJ_ALL);
            //---
            if(res==1){return;}
            //--- 如果对象是控制面板上注释栏位(Edit1)的操作:
            res=RestoringObjArrayAll(chartObject,prefixObj,flagClick,
                                     flagResetEventDelete,QUANT_OBJ_EDIT,
                                     nameEdit1,timeCreateEdit1,QUANT_OBJ_ALL);
            //---
            return;
           }
        }
      else if(flagClick==1)//如果按钮在最小化的面板上被修改或者删除
        {
         string clickedChartObject=sparam;
         if(StringCompare(clickedChartObject,nameButton[2],true)==0)
           {
            flagClick=-1;
            //--- 从图表上删除指标:
            ChIndicatorDelete(prefixObj,textDelInd[0],textDelInd[1],0,0);
            return;
           }
         return;
        }
      return;
     }//else  if(id==CHARTEVENT_OBJECT_DELETE || id==CHARTEVENT_OBJECT_CHANGE)

本代码块的新特性是当面板的画布修改或者删除时使用RestoringObjOneAll()函数. 这与之前使用的RestoringObjArrayAll()函数是类似的.
//+-----------------------------------------------------------------------+
//|string sparam = 传给 OnChartEvent() 的对象名称                 |
//|string prefix_obj = 被保护对象的通用前缀                 |
//|int &flag_antivandal = 启用/禁用警告的标志               |
//|int &flag_reset = 重设事件的标志                            |
//|string name = 对象名称                                           |
//|datetime time_create = 对象创建时间                       |
//|int quant_obj_all = 程序中可恢复对象的总数                |
//|>= quant_obj (如果是-1, 则等于 quant_obj)            |
//|const long chart_ID = 0 图表id                             |
//|const int sub_window = 0 窗口索引                                |
//|int start_pos=0 寻找前缀的对象名称位置 |
//+-----------------------------------------------------------------------+
int RestoringObjOneAll(const string sparam,
                       const string prefix_obj,
                       int &flag_antivandal,
                       int &flag_reset,
                       string name,
                       datetime time_create,
                       const int quant_obj_all,
                       const long chart_ID=0,
                       const int sub_window=0,
                       int start_pos=0
                       )
  {
   int res=-1;
   int comparison=-1;
//---
   comparison=StringCompare(sparam,name,true);
//--- 当名称完全匹配时:
   if(comparison==0)
     {
      res=1;
      comparison=-1;
      //--- 通知 "不安全" 发生的状况:
      Print(LINE_NUMBER,textRestoring,sparam);
      //---
      flag_antivandal=-1;
      flag_reset=quant_obj_all-1;
      //---
      ObDeletePrefixFlag(flag_reset,prefix_obj,chart_ID,sub_window,start_pos);
      //---
      ChartRedraw();
      //---
      ObDelCreateTimeExcept0(time_create,chart_ID,sub_window,-1);
      //---
      PanelCreate();
      //---
      ChartRedraw();
      //---
      flag_antivandal=0;
      //---
      return(res);
     }
   return(res);
  }

指标的完整代码附加在文章附件中, 名为: id_name_object.


6. 结论

您可以看出, 本文提到的基于标志的代码框架可以添加到写好的代码中, 如有必要或可能, 可以应用于一个或者多个项目. 并不需要修改大量代码, 也不会影响程序的主要运行过程.

如有必要, 还可以在代码中做如下的处理:

例如, 您可以在面板的"日常"操作中加入额外的检查, 在计时器中检查重要数据的修改次数,

举例来说, 在EA交易中, 如果不会严重影响交易的效率, 您可以在遇到未经授权的对图形对象有修改或者删除的时候显示动态信息, .

当显示信息时, 您可以提供相关的按钮: 从图表上删除程序或者尝试恢复被破坏的对象. 另外, 您还可以提供一个计数器, 可以在遇到未经授权的删除或者修改对象的时候显示如下的警告: 这是程序中的对象受到的第XX次干扰, 在受到XX次干扰之后, 程序会自己从图表上删除. 请找到造成这种情况的可能原因".

基于MQL5编程语言的特点, 当然, 还有您的知识和经验, 如有必要, 您可以提供更多选择, 包括未在本文中提到的方法.