从基础到进阶:对象事件(II)
引言
在上一篇文章《从基础到进阶:对象事件(I)》中,我们讲解并演示了基础部分,主要聚焦对象所产生的各类事件。不过,还有另外三类在用户与图表对象交互时可触发的事件有待探讨。这三类事件中,有一类必须手动显式启用,MetaTrader 5 才会真正生成该事件。否则,即便代码已经写好了事件处理逻辑,程序也永远收不到该事件已经发生的通知。
在组合使用这些事件前,理解每一类事件至关重要。本文将按如下方式讲解:分小节单独说明 MetaTrader 5 在何种场景、以何种方式生成剩下这三类事件。待你理解原理后,我们再来学习如何组合运用它们,实现一些有趣的功能。那么,我们开始吧。
CHARTEVENT_OBJECT_DELETE 事件
该事件和 CHARTEVENT_OBJECT_CREATE 以及其他鼠标相关事件一样,需要由我们的应用手动开启或关闭。这样做是为了让 MetaTrader 5 知晓:运行在图表上的应用,需要接收图表对象被删除的通知。
该事件主要用于预防。这点非常关键:如果我们希望在程序中维持特定的对象配置,借助这个事件,就可以防止用户删除图表内重要、关键的对象 —— 无论用户是删除对象时误操作,或是其他原因导致的删除行为。
原则上,这个事件本身十分简单。但是在编写和实现代码时,必须留意一些注意事项。之所以需要这些注意事项,是因为在某些场景下,我们会遇到和配色方案相关的问题。这最终会导致程序需要自动重建对象时,很难正确显示该对象。
不过本文并不会深入探讨上述问题。本文的目的,并不是讲解如何实现一套能够覆盖所有场景的通用解决方案。更何况,每种场景都各不相同。
目前不存在一套完整集成、通用的方法,可以重建被误删除的图表对象。作为开发者,你可以想出很多办法来实现这一需求。就我个人而言,建议使用一个小型结构体,用来保存由我们(也就是我们的应用程序)创建的对象属性。这样一来,当程序需要重建对象时,就可以按照对应的配色方案与位置信息来完成重建。除此之外,当然还包含字体、字号、对象尺寸等其他属性;我想这个思路已经很清楚了。
简短介绍到此结束,接下来你就可以查看本文的第一段代码。代码如下:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 07. //+------------------------------------------------------------------+ 08. string gl_szObjectName; 09. //+------------------------------------------------------------------+ 10. int OnInit() 11. { 12. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 13. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 14. 15. gl_szObjectName = macro_NameObject; 16. Proc_Object(); 17. 18. return INIT_SUCCEEDED; 19. }; 20. //+------------------------------------------------------------------+ 21. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 22. { 23. return rates_total; 24. }; 25. //+------------------------------------------------------------------+ 26. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 27. { 28. switch (id) 29. { 30. case CHARTEVENT_MOUSE_MOVE: 31. ObjectSetString(0, gl_szObjectName, OBJPROP_TEXT, StringFormat("%03d : %03d", (ushort)lparam, (ushort)dparam)); 32. break; 33. case CHARTEVENT_OBJECT_DELETE: 34. if (sparam == gl_szObjectName) Proc_Object(); 35. break; 36. } 37. ChartRedraw(); 38. }; 39. //+------------------------------------------------------------------+ 40. void OnDeinit(const int reason) 41. { 42. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 43. ObjectsDeleteAll(0, def_Prefix); 44. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 45. ChartRedraw(); 46. }; 47. //+------------------------------------------------------------------+ 48. void Proc_Object(void) 49. { 50. ObjectCreate(0, gl_szObjectName, OBJ_LABEL, 0, 0, 0); 51. ObjectSetInteger(0, gl_szObjectName, OBJPROP_SELECTABLE, true); 52. ObjectSetInteger(0, gl_szObjectName, OBJPROP_XDISTANCE, 50); 53. ObjectSetInteger(0, gl_szObjectName, OBJPROP_YDISTANCE, 50); 54. ObjectSetInteger(0, gl_szObjectName, OBJPROP_XSIZE, 150); 55. ObjectSetInteger(0, gl_szObjectName, OBJPROP_COLOR, clrMediumBlue); 56. ObjectSetInteger(0, gl_szObjectName, OBJPROP_FONTSIZE, 20); 57. ObjectSetString(0, gl_szObjectName, OBJPROP_FONT, "Lucida Console"); 58. } 59. //+------------------------------------------------------------------+
代码 01
代码 01 的目的十分简单:演示从图表上删除对象时会发生什么。我们需要开启、关闭事件通知,让 MetaTrader 5 告知我们对象是否被删除,借助这段代码,能更容易理解整套机制的工作原理。
最难的部分或许并不是开启通知 —— 这个操作非常简单,只需要第 13 行代码。通常我们会在程序刚启动时就开启通知,避免遗漏任何可能触发的删除事件。真正的难点在于确定何时关闭该通知。MetaTrader 5 有其固有的运行机制;关闭通知的时机不同,会得到一些看起来很奇怪的结果,不过大多数情况下这类结果并不会造成损害。
想要理解这一点,可以尝试改动第 42 行的代码位置。这一行的作用是关闭图表对象删除事件的通知。源代码就在程序内,你可以试着交换第 42 行与第 43 行的代码内容。改动后,将指标从图表移除时,你会看到一个很有意思的现象。由此就能发现,这些操作并不是可以随意调换执行顺序。这也能帮你理解很多单用文字很难讲清楚的问题。
无论如何,运行代码 01 时,我们基本上可以执行两类操作。第一个示例可参考下面的动图:

动图 01
在这个场景里,我们选中对象,按下 Delete 键删除。请注意:按下 Delete 键后对象会立刻重建。但由于信息更新绑定了鼠标事件,需要移动鼠标或触发鼠标事件,相关信息才会正常显示。第二种场景:通过对象列表窗口尝试删除对象。参考下面的动图:

动图 02
和动图 01 的现象一样,在动图 02 中可以清晰看到:我们删除对象的瞬间,对象就完成重建。即便如此,对象列表窗口不会立刻刷新显示。需要重新打开窗口,重建后的对象才会再次出现在图表对象列表中。
综上,该事件的作用就是通知我们:图表上某个对象已被删除。接下来我们还要讲解另外两种事件。这里会引出一些很值得探讨的问题。为更好地说明,我们开启新小节。
CHARTEVENT_OBJECT_CHANGE 事件
如果弄懂原理,你会发现我们接下来要介绍的这个事件非常实用。因为每当对象的任意属性发生变化时,该事件就会触发,我们可以依托不同场景,实现不少有意思的功能。
上一节提到,不存在能 100% 通用的机制来恢复被删除的对象。这个说法是成立的。但根据代码实现方式的不同,我们可以找到合适方案,用相对通用的逻辑重建几乎所有类型的对象。从目前演示的内容来看,这是一项相当复杂的工作。不必心急:虽然依靠相对简单的机制就能写出不错的程序,但未来我们还会找到更优的实现方案。知识与经验,都是逐步积累起来的。所以持续学习和实操练习永远有益。不必急于求成,但要坚持学习。
为展示该事件的价值,以及精准理解代码实现逻辑的重要性,我们将修改上一节的代码 01,以此演示本节介绍的事件。修改后的代码如下:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 07. //+------------------------------------------------------------------+ 08. struct st_Obj 09. { 10. //+----------------+ 11. private: 12. string szName, 13. font; 14. bool Selectable; 15. int x, 16. y, 17. w, 18. fontSize; 19. color cor; 20. //+----------------+ 21. public: 22. //+----------------+ 23. void SetDefault(const string arg) 24. { 25. szName = arg; 26. font = "Lucida Console"; 27. Selectable = true; 28. x = 50; 29. y = 50; 30. w = 150; 31. cor = clrMediumBlue; 32. fontSize = 20; 33. } 34. //+----------------+ 35. void Create(void) 36. { 37. ObjectCreate(0, szName, OBJ_LABEL, 0, 0, 0); 38. ObjectSetInteger(0, szName, OBJPROP_SELECTABLE, Selectable); 39. ObjectSetInteger(0, szName, OBJPROP_XDISTANCE, x); 40. ObjectSetInteger(0, szName, OBJPROP_YDISTANCE, y); 41. ObjectSetInteger(0, szName, OBJPROP_XSIZE, w); 42. ObjectSetInteger(0, szName, OBJPROP_COLOR, cor); 43. ObjectSetInteger(0, szName, OBJPROP_FONTSIZE, fontSize); 44. ObjectSetString(0, szName, OBJPROP_FONT, font); 45. } 46. //+----------------+ 47. string GetName(void) 48. { 49. return szName; 50. } 51. //+----------------+ 52. }gl_Obj; 53. //+------------------------------------------------------------------+ 54. int OnInit() 55. { 56. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 57. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 58. 59. gl_Obj.SetDefault(macro_NameObject); 60. gl_Obj.Create(); 61. 62. return INIT_SUCCEEDED; 63. }; 64. //+------------------------------------------------------------------+ 65. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 66. { 67. return rates_total; 68. }; 69. //+------------------------------------------------------------------+ 70. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 71. { 72. switch (id) 73. { 74. case CHARTEVENT_MOUSE_MOVE: 75. ObjectSetString(0, gl_Obj.GetName(), OBJPROP_TEXT, StringFormat("%03d : %03d", (ushort)lparam, (ushort)dparam)); 76. break; 77. case CHARTEVENT_OBJECT_DELETE: 78. if (sparam == gl_Obj.GetName()) gl_Obj.Create(); 79. break; 80. case CHARTEVENT_OBJECT_CHANGE: 81. Comment("An event occurred: CHARTEVENT_OBJECT_CHANGE in object ["+sparam+"]"); 82. break; 83. } 84. ChartRedraw(); 85. }; 86. //+------------------------------------------------------------------+ 87. void OnDeinit(const int reason) 88. { 89. Comment(""); 90. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 91. ObjectsDeleteAll(0, def_Prefix); 92. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 93. ChartRedraw(); 94. }; 95. //+------------------------------------------------------------------+
代码 02
运行代码 02 时,我们会观察到两种现象。第一种现象见下面的动图。

动图 03
请注意:只要我们修改对象的任意一项属性,就会触发事件,该事件会在第 80 行对应的分支中被捕获。由于我们没有对事件来源做过滤,任意对象的任何属性发生改动,都会在图表的左上角输出第 81 行对应的提示信息,动图 03 中就可以看到该效果。但最有意思的现象出现在第二个事件触发时,参见动图 04:

动图 04
“真是奇怪。为什么对象没有保留修改后的属性?”我们调用了第 35 行的过程函数。对于代码 01 的情况我能理解原因,但在本案例中我以为:如果对象从图表被删除,程序会使用完全相同的属性将它重建。代码并没有调用 SetDefault 函数去还原动图 03 中被修改过的各项数值。在我看来,这一点十分反常。”
亲爱的读者,很多编程新手都会掉进同一个陷阱:写了代码,却并不理解代码背后的部分内部机制。这种情况往往出现在直接复制别处代码的时候。但也会发生在我们测试新功能实现的时候 —— 看起来一切都完美无误,测试阶段却发现还有部分细节并未实现。无论如何,这并不算错误;恰恰相反。这是练习、验证新方案的很好方式,这些新方案未来可能会发挥价值。
好了,那问题到底出在哪?关键点在于:你在 MetaTrader 5 中修改对象的某项属性时,并不会更新对象创建时所使用的原始属性。你修改的仅仅是该对象当前的属性值。因此,当对象被删除,代码 02 的第 80 行重新创建该对象时,重建所使用的,依旧是保存在全局变量里的原始参数与属性。
所以,重建出来的对象完全是代码里预设的配置状态。如果想要保留用户做出的修改,我们就必须在程序内部同步更新该对象,把发生的改动记录下来。而这就需要借助第 82 行完成的事件捕获逻辑。这也是代码 02 中我们没有对事件来源进行过滤的原因。我希望你理解:程序是如何接收到通知,得知图表上某个对象的属性发生了改变。
MetaTrader 只会告诉我们被修改对象的名称,并不会告知具体是哪几项属性发生了变化,因此我们需要捕获全部改动。至少要捕获我们真正关心的那些属性。虽然很多图表对象拥有相似属性,但也存在部分属性为特定对象独有,其他对象并不具备。这就是为什么我们之前说,实现一套 100% 通用的解决方案是一件难度很高的工作。
明白这一点之后,我们就可以修改代码 02,让部分属性得以保留。至少在程序运行期间可以保留。一旦程序从图表上移除,用户修改的属性就会全部丢失,对象会恢复为代码中预先定义的配置。解决该问题有多种实现思路。不过这个话题我们留到下次再讨论。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 007. //+------------------------------------------------------------------+ 008. struct st_Obj 009. { 010. //+----------------+ 011. private: 012. string szName, 013. font; 014. bool Selectable; 015. int x, 016. y, 017. w, 018. fontSize; 019. color cor; 020. //+----------------+ 021. public: 022. //+----------------+ 023. void SetDefault(const string arg) 024. { 025. szName = arg; 026. font = "Lucida Console"; 027. Selectable = true; 028. x = 50; 029. y = 50; 030. w = 150; 031. cor = clrMediumBlue; 032. fontSize = 20; 033. } 034. //+----------------+ 035. void Create(void) 036. { 037. ObjectCreate(0, szName, OBJ_LABEL, 0, 0, 0); 038. ObjectSetInteger(0, szName, OBJPROP_SELECTABLE, Selectable); 039. ObjectSetInteger(0, szName, OBJPROP_XDISTANCE, x); 040. ObjectSetInteger(0, szName, OBJPROP_YDISTANCE, y); 041. ObjectSetInteger(0, szName, OBJPROP_XSIZE, w); 042. ObjectSetInteger(0, szName, OBJPROP_COLOR, cor); 043. ObjectSetInteger(0, szName, OBJPROP_FONTSIZE, fontSize); 044. ObjectSetString(0, szName, OBJPROP_FONT, font); 045. } 046. //+----------------+ 047. string GetName(void) 048. { 049. return szName; 050. } 051. //+----------------+ 052. void Update(void) 053. { 054. font = ObjectGetString(0, szName, OBJPROP_FONT); 055. Selectable = (int)ObjectGetInteger(0, szName, OBJPROP_SELECTABLE); 056. x = (int)ObjectGetInteger(0, szName, OBJPROP_XDISTANCE); 057. y = (int)ObjectGetInteger(0, szName, OBJPROP_YDISTANCE); 058. w = (int)ObjectGetInteger(0, szName, OBJPROP_XSIZE); 059. fontSize = (int)ObjectGetInteger(0, szName, OBJPROP_FONTSIZE); 060. cor = (color)ObjectGetInteger(0, szName, OBJPROP_COLOR); 061. } 062. //+----------------+ 063. }gl_Obj; 064. //+------------------------------------------------------------------+ 065. int OnInit() 066. { 067. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 068. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 069. 070. gl_Obj.SetDefault(macro_NameObject); 071. gl_Obj.Create(); 072. 073. return INIT_SUCCEEDED; 074. }; 075. //+------------------------------------------------------------------+ 076. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 077. { 078. return rates_total; 079. }; 080. //+------------------------------------------------------------------+ 081. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 082. { 083. switch (id) 084. { 085. case CHARTEVENT_MOUSE_MOVE: 086. ObjectSetString(0, gl_Obj.GetName(), OBJPROP_TEXT, StringFormat("%03d : %03d", (ushort)lparam, (ushort)dparam)); 087. break; 088. case CHARTEVENT_OBJECT_DELETE: 089. if (sparam == gl_Obj.GetName()) gl_Obj.Create(); 090. break; 091. case CHARTEVENT_OBJECT_CHANGE: 092. if (sparam == gl_Obj.GetName()) gl_Obj.Update(); 093. break; 094. } 095. ChartRedraw(); 096. }; 097. //+------------------------------------------------------------------+ 098. void OnDeinit(const int reason) 099. { 100. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 101. ObjectsDeleteAll(0, def_Prefix); 102. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 103. ChartRedraw(); 104. }; 105. //+------------------------------------------------------------------+
代码 03
代码 03 展示了需要实现的逻辑。代码运行效果可参考下面的动图:

动图 05
请注意:现在重建对象时,会使用用户几分钟之前自定义的属性。即便对象从图表中被删除,由于该对象十分重要,程序会自动重建它。但和之前不同,重建对象不再使用程序内预设的属性,而是读取用户设置并由第 92 行代码保存下来的属性。
但请注意,本次代码加入了过滤逻辑。考虑到我们正在实现的功能本身,这一步至关重要。设想如下场景:图表上存在两个对象,你修改其中一个对象的属性。到这里一切正常。但是,当我们程序管控的对象被删除时,它会立刻重建。倘若没有第 92 行的过滤逻辑,当修改另一个不受本程序管控的对象时,该对象的属性就会被错误赋值给程序重建出来的对象,这会导致图表上所有对象相关操作彻底混乱。这就是第 92 行的过滤代码对我们如此重要的原因。
你可以测试不带第 92 行过滤逻辑的代码 03,观察运行结果。有些场景下这种效果也有它的用处。所以读者朋友,理解并观察这个现象,在未来某些时刻会对你有所帮助。
我们不只是在写对代码的时候才学到知识。
代码运行不符合预期的时候,同样也是学习的过程。
在这种情况下,理解解决方案的推导过程,和方案本身同等重要。
好了,现在我们掌握了足够的知识,可以来研究最后一类对象事件。本次我们针对单一对象,研究与该对象直接相关的事件,并做一个小实验。在我看来,这个实验非常有意思,而且极具教学意义。我们要实现 MetaTrader 5 默认并不具备的功能。但是可以通过编写程序实现。结合这套实现逻辑来看,会非常有趣。
接下来开启新小节,把内容划分清楚。
CHARTEVENT_OBJECT_ENDEDIT 事件
我发现一件很有意思的事:很多用户根本不知道,我们能对 MetaTrader 5 做多大程度的自定义开发,来实现各类任务。很多功能,人们固执地认为不可能实现、不切实际。
本质上这里有一个问题:一方面,这个需求看起来比较特殊;另一方面,如果直接按常规方式实现,意义不大。最重要的一点是要有耐心。和很多人的固有认知相反,MQL5 并不只是一门专门用来开发 MetaTrader 5 应用的语言。虽然大体上,它确实主要服务于这个用途。本质而言,MQL5 赋予我们一定程度的控制权,让我们可以自定义 MetaTrader 5 的运行方式,或者说按照我们想要的方式运行。当然,一切都需要遵守相应的规则。我之所以这么说,是因为如果搞不懂这个简单核心的要点,就很难理解本节我们要做的功能。
绝大多数用户,尤其是新手,会觉得接下来要实现的内容简直超乎想象。甚至会以为这是 AI 或者外星人才写得出来的代码。但事实并非如此。实现这套功能不需要很深的知识储备,更多依靠想象力、创造力。当然还需要细心观察。
不知道你有没有留意:OBJ_LABEL 对象和 OBJ_EDIT 对象非常相似。大体来说,二者最主要,甚至可以说是唯一的区别是:OBJ_EDIT 支持直接在图表上编辑文本,而 OBJ_LABEL 对象不支持。但事实真的是这样吗?
亲爱的读者,真相是:虽然两者外观很像,但OBJ_EDIT 和 OBJ_LABEL 的特性并不相同,反之亦然。这是从用户视角来看的情况。但如果站在编程角度,情况就不一样了。从代码层面看,这两个对象本质上很接近,只要找对思路,甚至可以协同工作。
我们本节要实现的功能如下:找到一种方法,让 OBJ_LABEL 对象支持直接在图表上编辑文本,而不用打开属性窗口输入文字。听起来不可能?那我们看看如何实现。记住,这里的目的是教学,不必过度纠结代码具体怎么写。重点是弄懂这套机制为什么能够生效。这才是最关键的。
首先,我们需要源代码。我们不想从零开始编写大量代码,花费大量时间讲解每一步操作,所以基于上一节的代码做修改。代码如下:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 007. //+------------------------------------------------------------------+ 008. struct st_Obj 009. { 010. //+----------------+ 011. private: 012. string szName, 013. font; 014. bool Selectable; 015. int x, 016. y, 017. w, 018. h, 019. fontSize; 020. color cor, 021. backColor; 022. ENUM_OBJECT actual; 023. //+----------------+ 024. public: 025. //+----------------+ 026. void SetDefault(const string arg) 027. { 028. szName = arg; 029. font = "Lucida Console"; 030. Selectable = true; 031. x = 50; 032. y = 50; 033. w = 150; 034. h = 27; 035. cor = clrMediumBlue; 036. backColor = clrWhite; 037. fontSize = 20; 038. } 039. //+----------------+ 040. void Create(const ENUM_OBJECT type) 041. { 042. actual = type; 043. Recreates(); 044. } 045. //+----------------+ 046. void Recreates(void) 047. { 048. ObjectCreate(0, szName, actual, 0, 0, 0); 049. ObjectSetInteger(0, szName, OBJPROP_SELECTABLE, (actual != OBJ_EDIT ? Selectable : false)); 050. ObjectSetInteger(0, szName, OBJPROP_XDISTANCE, x); 051. ObjectSetInteger(0, szName, OBJPROP_YDISTANCE, y); 052. ObjectSetInteger(0, szName, OBJPROP_XSIZE, w); 053. ObjectSetInteger(0, szName, OBJPROP_YSIZE, h); 054. ObjectSetInteger(0, szName, OBJPROP_COLOR, cor); 055. ObjectSetInteger(0, szName, OBJPROP_FONTSIZE, fontSize); 056. ObjectSetString(0, szName, OBJPROP_FONT, font); 057. if (actual == OBJ_EDIT) 058. { 059. ObjectSetInteger(0, szName, OBJPROP_BGCOLOR, backColor); 060. ObjectSetInteger(0, szName, OBJPROP_READONLY, false); 061. } 062. } 063. //+----------------+ 064. string GetName(void) 065. { 066. return szName; 067. } 068. //+----------------+ 069. void Update(void) 070. { 071. font = ObjectGetString(0, szName, OBJPROP_FONT); 072. Selectable = (int)ObjectGetInteger(0, szName, OBJPROP_SELECTABLE); 073. x = (int)ObjectGetInteger(0, szName, OBJPROP_XDISTANCE); 074. y = (int)ObjectGetInteger(0, szName, OBJPROP_YDISTANCE); 075. w = (int)ObjectGetInteger(0, szName, OBJPROP_XSIZE); 076. h = (int)ObjectGetInteger(0, szName, OBJPROP_YSIZE); 077. fontSize = (int)ObjectGetInteger(0, szName, OBJPROP_FONTSIZE); 078. cor = (color)ObjectGetInteger(0, szName, OBJPROP_COLOR); 079. backColor = (color)((ENUM_OBJECT)ObjectGetInteger(0, szName, OBJPROP_TYPE) == OBJ_LABEL ? backColor : ObjectGetInteger(0, szName, OBJPROP_BGCOLOR)); 080. } 081. //+----------------+ 082. }gl_Obj; 083. //+------------------------------------------------------------------+ 084. int OnInit() 085. { 086. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 087. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 088. 089. gl_Obj.SetDefault(macro_NameObject); 090. gl_Obj.Create(OBJ_LABEL); 091. 092. return INIT_SUCCEEDED; 093. }; 094. //+------------------------------------------------------------------+ 095. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 096. { 097. return rates_total; 098. }; 099. //+------------------------------------------------------------------+ 100. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 101. { 102. switch (id) 103. { 104. case CHARTEVENT_MOUSE_MOVE : 105. ObjectSetString(0, gl_Obj.GetName(), OBJPROP_TEXT, StringFormat("%03d : %03d", (ushort)lparam, (ushort)dparam)); 106. break; 107. case CHARTEVENT_OBJECT_DELETE : 108. if (sparam == gl_Obj.GetName())gl_Obj.Recreates(); 109. break; 110. case CHARTEVENT_OBJECT_CHANGE : 111. if (sparam == gl_Obj.GetName()) gl_Obj.Update(); 112. break; 113. } 114. ChartRedraw(); 115. }; 116. //+------------------------------------------------------------------+ 117. void OnDeinit(const int reason) 118. { 119. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 120. ObjectsDeleteAll(0, def_Prefix); 121. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 122. ChartRedraw(); 123. }; 124. //+------------------------------------------------------------------+
代码 04
我们在这里新增了一些内容。但需要注意:代码 04 的运行方式和代码 03 大体相同。留意第 40 行:调用创建对象的过程函数时,我们需要指定待创建对象的类型。在本案例中,我们会用到 OBJ_EDIT 和 OBJ_LABEL 这两种对象。通过这些修改,我们开始搭建另一项功能的基础,你很快就能理解它的核心原理:操控 OBJ_LABEL 对象,实现直接在图表上编辑文本。
接下来请留意代码 04 里的另一个关键点。在真正创建对象的第 49 行,我们设置了 OBJPROP_SELECTABLE 属性。这个属性十分关键,必须正确配置。对于 OBJ_LABEL 对象:如果后续需要选中该对象(就像前面示例那样),就需要把这个属性设为 true。但如果在 OBJ_EDIT 对象中将该属性设为 true,就无法直接在图表上编辑文本。
前面提到过,每种对象都有自身的特性。根据我们想要执行的操作,需要按特定方式设置对象属性。简单总结:当 OBJ_EDIT 对象的 OBJPROP_SELECTABLE 属性为 true 时,不能编辑文本,但可以在图表上拖动移动对象。如果把该属性设置为 false,则可以编辑文本,但无法移动对象。
请看第 90 行,这里指定了我们想要创建的对象类型,也就是 OBJ_LABEL。但第 108 行不再调用 Create 创建函数,而是调用另一个函数来重建对象。原因是我们代码里已经明确知道要创建哪一类对象;但程序无法直接获取 —— 如果用查询方式向 MT5 获取对象类型,得到的结果几乎总是错误的。这是因为 MetaTrader 5 已经把该对象销毁了。
当我们收到 CHARTEVENT_OBJECT_DELETE 事件时,MT5 销毁对象并不会征求许可。触发该事件的时候,对象其实已经被 MT5 删除。因此,除非使用零类型对象(即 OBJ_VLINE),否则如果向 MetaTrader 5 查询,我们永远无法获知被删除的对象原本是什么类型。MT5 无法给出信息,因为该对象已经不存在了。
到这里为止,我认为所有内容都简单易懂。接下来是最有意思的部分:我们将修改代码 04,让多个事件协同工作,以此实现目标效果。
一次性展示全部代码会显得比较复杂,所以我们拆成两个简单步骤实现。第一个示例代码如下。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 007. //+------------------------------------------------------------------+ 008. struct st_Obj 009. { 010. //+----------------+ 011. private: 012. string szName, 013. font, 014. text; 015. bool Selectable; 016. int x, 017. y, 018. w, 019. h, 020. fontSize; 021. color cor, 022. backColor; 023. ENUM_OBJECT actual; 024. //+----------------+ 025. public: 026. //+----------------+ 027. void SetDefault(const string arg) 028. { 029. szName = arg; 030. font = "Lucida Console"; 031. Selectable = true; 032. x = 50; 033. y = 50; 034. w = 250; 035. h = 27; 036. cor = clrMediumBlue; 037. backColor = clrWhite; 038. fontSize = 20; 039. } 040. //+----------------+ 041. void Create(const ENUM_OBJECT type) 042. { 043. actual = type; 044. text = EnumToString(type); 045. Recreates(); 046. } 047. //+----------------+ 048. void Recreates(void) 049. { 050. ObjectCreate(0, szName, actual, 0, 0, 0); 051. ObjectSetInteger(0, szName, OBJPROP_SELECTABLE, (actual != OBJ_EDIT ? Selectable : false)); 052. ObjectSetInteger(0, szName, OBJPROP_XDISTANCE, x); 053. ObjectSetInteger(0, szName, OBJPROP_YDISTANCE, y); 054. ObjectSetInteger(0, szName, OBJPROP_XSIZE, w); 055. ObjectSetInteger(0, szName, OBJPROP_YSIZE, h); 056. ObjectSetInteger(0, szName, OBJPROP_COLOR, cor); 057. ObjectSetInteger(0, szName, OBJPROP_FONTSIZE, fontSize); 058. ObjectSetString(0, szName, OBJPROP_FONT, font); 059. ObjectSetString(0, szName, OBJPROP_TEXT, text); 060. if (actual == OBJ_EDIT) 061. { 062. ObjectSetInteger(0, szName, OBJPROP_BGCOLOR, backColor); 063. ObjectSetInteger(0, szName, OBJPROP_READONLY, false); 064. } 065. } 066. //+----------------+ 067. string GetName(void) 068. { 069. return szName; 070. } 071. //+----------------+ 072. void Update(void) 073. { 074. font = ObjectGetString(0, szName, OBJPROP_FONT); 075. Selectable = (int)ObjectGetInteger(0, szName, OBJPROP_SELECTABLE); 076. x = (int)ObjectGetInteger(0, szName, OBJPROP_XDISTANCE); 077. y = (int)ObjectGetInteger(0, szName, OBJPROP_YDISTANCE); 078. w = (int)ObjectGetInteger(0, szName, OBJPROP_XSIZE); 079. h = (int)ObjectGetInteger(0, szName, OBJPROP_YSIZE); 080. fontSize = (int)ObjectGetInteger(0, szName, OBJPROP_FONTSIZE); 081. cor = (color)ObjectGetInteger(0, szName, OBJPROP_COLOR); 082. backColor = (color)((ENUM_OBJECT)ObjectGetInteger(0, szName, OBJPROP_TYPE) == OBJ_LABEL ? backColor : ObjectGetInteger(0, szName, OBJPROP_BGCOLOR)); 083. } 084. //+----------------+ 085. }gl_Obj; 086. //+------------------------------------------------------------------+ 087. int OnInit() 088. { 089. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 090. 091. gl_Obj.SetDefault(macro_NameObject); 092. gl_Obj.Create(OBJ_LABEL); 093. 094. return INIT_SUCCEEDED; 095. }; 096. //+------------------------------------------------------------------+ 097. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 098. { 099. return rates_total; 100. }; 101. //+------------------------------------------------------------------+ 102. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 103. { 104. switch (id) 105. { 106. case CHARTEVENT_OBJECT_CLICK : 107. if (sparam == gl_Obj.GetName()) 108. { 109. string sz0 = ObjectGetString(0, sparam, OBJPROP_TEXT); 110. if ((ENUM_OBJECT)ObjectGetInteger(0, sparam, OBJPROP_TYPE) == OBJ_EDIT) 111. break; 112. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 113. ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true); 114. ObjectDelete(0, sparam); 115. gl_Obj.Create(OBJ_EDIT); 116. ObjectSetString(0, sparam, OBJPROP_TEXT, sz0); 117. } 118. break; 119. case CHARTEVENT_OBJECT_CREATE : 120. if (sparam == gl_Obj.GetName()) 121. { 122. ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, false); 123. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 124. } 125. break; 126. case CHARTEVENT_OBJECT_DELETE : 127. if (sparam == gl_Obj.GetName())gl_Obj.Recreates(); 128. break; 129. case CHARTEVENT_OBJECT_CHANGE : 130. if (sparam == gl_Obj.GetName()) gl_Obj.Update(); 131. break; 132. } 133. ChartRedraw(); 134. }; 135. //+------------------------------------------------------------------+ 136. void OnDeinit(const int reason) 137. { 138. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 139. ObjectsDeleteAll(0, def_Prefix); 140. ChartRedraw(); 141. }; 142. //+------------------------------------------------------------------+
代码 05
在代码 05 中,会出现一个非常奇妙的现象。你可以参考下面这张动图查看效果:

动图 06
读者朋友,请仔细观察动图 06。注意第 92 行,这里请求创建一个 OBJ_LABEL 对象。正因如此,第 44 行设置了文本,用来标识当前使用的对象类型。到这里为止,一切都按常规运行。但是,只要点击这个 OBJ_LABEL 对象,它就会转换为 OBJ_EDIT 对象。该转换逻辑正是由第 106 行捕获事件的处理代码实现的。请注意,我们只针对最开始创建的这个对象进行操作。
而得益于第 109 行代码,OBJ_LABEL 对象内的文本会在第 116 行被传递给 OBJ_EDIT 对象。这一步至关重要,如果不做文本传递,OBJ_EDIT 对象里就会显示其他文本。这并不是我们想要的效果。我们真正的目标,是可以直接在图表上修改 OBJ_LABEL 对象的文字。
接下来请注意:在 CHARTEVENT_OBJECT_CLICK 事件的处理逻辑里,我们先删除 OBJ_LABEL 对象,再新建 OBJ_EDIT 对象。但因为我们已经在第 89 行开启了 CHARTEVENT_OBJECT_DELETE 事件通知,所以需要临时关闭它,这一步在第 112 行完成。紧接着,我们开启另一个事件,用来告知 MetaTrader 5,程序需要接收对象创建的通知。这样一来,程序新建对象触发的创建事件,就会在第 119 行被捕获。在这个事件处理函数内,我们关闭对象创建通知,并重新开启对象删除通知。
经过这样处理,第 126 行的事件处理函数就可以再次运行,不过还不算尽善尽美。原因是:如果删除 OBJ_EDIT 对象,程序虽然会重建对象,但重建后的文本和对象销毁前原本的文本不再一致。
这个小问题就留给你当作练习,你可以自行寻找方案解决这个小缺陷。实现起来并不难。但要记住,需要处理部分数据,才能还原创建 OBJ_EDIT 那一刻的文本。而如果想要保留 OBJ_EDIT 对象从创建到删除期间修改过的文本,则需要更多开发工作。你还需要处理键盘和鼠标事件,在内存里持续更新文本内容,才能复原修改后的文字。
我个人认为后面这部分完全没必要实现,甚至有些麻烦。毕竟,如果用户在把文本保存到 OBJ_LABEL 对象之前就删掉了内容,那就让用户自己记住 OBJ_EDIT 里原来写了什么吧。(笑)。
好了,到了最后一部分,也就是使用本节介绍的事件。为此,我们要对代码做最后一次修改。这可以在下面看到。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 007. //+------------------------------------------------------------------+ 008. struct st_Obj 009. { 010. //+----------------+ 011. private: 012. string szName, 013. font, 014. text; 015. bool Selectable; 016. int x, 017. y, 018. w, 019. h, 020. fontSize; 021. color cor, 022. backColor; 023. ENUM_OBJECT actual; 024. //+----------------+ 025. public: 026. //+----------------+ 027. void SetDefault(const string arg) 028. { 029. szName = arg; 030. font = "Lucida Console"; 031. Selectable = true; 032. x = 50; 033. y = 50; 034. w = 250; 035. h = 27; 036. cor = clrMediumBlue; 037. backColor = clrWhite; 038. fontSize = 20; 039. } 040. //+----------------+ 041. void Create(const ENUM_OBJECT type) 042. { 043. actual = type; 044. text = EnumToString(type); 045. Recreates(); 046. } 047. //+----------------+ 048. void Recreates(void) 049. { 050. ObjectCreate(0, szName, actual, 0, 0, 0); 051. ObjectSetInteger(0, szName, OBJPROP_SELECTABLE, (actual != OBJ_EDIT ? Selectable : false)); 052. ObjectSetInteger(0, szName, OBJPROP_XDISTANCE, x); 053. ObjectSetInteger(0, szName, OBJPROP_YDISTANCE, y); 054. ObjectSetInteger(0, szName, OBJPROP_XSIZE, w); 055. ObjectSetInteger(0, szName, OBJPROP_YSIZE, h); 056. ObjectSetInteger(0, szName, OBJPROP_COLOR, cor); 057. ObjectSetInteger(0, szName, OBJPROP_FONTSIZE, fontSize); 058. ObjectSetString(0, szName, OBJPROP_FONT, font); 059. ObjectSetString(0, szName, OBJPROP_TEXT, text); 060. if (actual == OBJ_EDIT) 061. { 062. ObjectSetInteger(0, szName, OBJPROP_BGCOLOR, backColor); 063. ObjectSetInteger(0, szName, OBJPROP_READONLY, false); 064. } 065. } 066. //+----------------+ 067. string GetName(void) 068. { 069. return szName; 070. } 071. //+----------------+ 072. void Update(void) 073. { 074. font = ObjectGetString(0, szName, OBJPROP_FONT); 075. Selectable = (int)ObjectGetInteger(0, szName, OBJPROP_SELECTABLE); 076. x = (int)ObjectGetInteger(0, szName, OBJPROP_XDISTANCE); 077. y = (int)ObjectGetInteger(0, szName, OBJPROP_YDISTANCE); 078. w = (int)ObjectGetInteger(0, szName, OBJPROP_XSIZE); 079. h = (int)ObjectGetInteger(0, szName, OBJPROP_YSIZE); 080. fontSize = (int)ObjectGetInteger(0, szName, OBJPROP_FONTSIZE); 081. cor = (color)ObjectGetInteger(0, szName, OBJPROP_COLOR); 082. backColor = (color)((ENUM_OBJECT)ObjectGetInteger(0, szName, OBJPROP_TYPE) == OBJ_LABEL ? backColor : ObjectGetInteger(0, szName, OBJPROP_BGCOLOR)); 083. text = ObjectGetString(0, szName, OBJPROP_TEXT); 084. } 085. //+----------------+ 086. }gl_Obj; 087. //+------------------------------------------------------------------+ 088. int OnInit() 089. { 090. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 091. 092. gl_Obj.SetDefault(macro_NameObject); 093. gl_Obj.Create(OBJ_LABEL); 094. 095. return INIT_SUCCEEDED; 096. }; 097. //+------------------------------------------------------------------+ 098. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 099. { 100. return rates_total; 101. }; 102. //+------------------------------------------------------------------+ 103. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 104. { 105. //+----------------+ 106. #define macro_SWAP(A) { \ 107. string sz0 = ObjectGetString(0, sparam, OBJPROP_TEXT); \ 108. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); \ 109. ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true); \ 110. ObjectDelete(0, sparam); \ 111. gl_Obj.Create(A); \ 112. ObjectSetString(0, sparam, OBJPROP_TEXT, sz0); \ 113. gl_Obj.Update(); \ 114. } 115. //+----------------+ 116. switch (id) 117. { 118. case CHARTEVENT_OBJECT_CLICK : 119. if (sparam == gl_Obj.GetName()) 120. if ((ENUM_OBJECT)ObjectGetInteger(0, sparam, OBJPROP_TYPE) != OBJ_EDIT) 121. macro_SWAP(OBJ_EDIT); 122. break; 123. case CHARTEVENT_OBJECT_CREATE : 124. if (sparam == gl_Obj.GetName()) 125. { 126. ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, false); 127. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true); 128. } 129. break; 130. case CHARTEVENT_OBJECT_DELETE : 131. if (sparam == gl_Obj.GetName())gl_Obj.Recreates(); 132. break; 133. case CHARTEVENT_OBJECT_CHANGE : 134. if (sparam == gl_Obj.GetName()) gl_Obj.Update(); 135. break; 136. case CHARTEVENT_OBJECT_ENDEDIT : 137. if (sparam == gl_Obj.GetName()) macro_SWAP(OBJ_LABEL) 138. break; 139. } 140. ChartRedraw(); 141. //+----------------+ 142. #undef macro_SWAP 143. //+----------------+ 144. }; 145. //+------------------------------------------------------------------+ 146. void OnDeinit(const int reason) 147. { 148. ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false); 149. ObjectsDeleteAll(0, def_Prefix); 150. ChartRedraw(); 151. }; 152. //+------------------------------------------------------------------+
代码 06
代码 06 已经实现了多项功能,其中就包含上述小问题的修复方案。但我们刻意改动了一部分代码,隐藏了具体的解决办法。这样做是为了避免经验尚浅的读者一眼就看懂实现思路。
代码 06 和前面几份代码一样简洁,只要认真学习、动手练习,任何人都能轻松解决这个问题,因此我不打算过多讲解。只说明这一点:当 OBJ_EDIT 对象结束编辑(通常按下回车键时),MetaTrader 5 会触发 CHARTEVENT_OBJECT_ENDEDIT 事件。该事件会在第 136 行被捕获,原本的 OBJ_EDIT 对象就会再次转换回 OBJ_LABEL 对象。
一种运行效果如下:

动图 07
读者朋友,不要被动图 07 的表面现象误导。试着研究并理解底层真正发生的逻辑。你在动图 07 里看到的,仅仅是巨大冰山露出水面的一角。本文只介绍了这件事最基础的部分。我们还可以在此基础上实现更多高级功能。
后记
很多读者可能觉得本文略显枯燥,因为文中反复贴出完整代码,对原理只做简要说明。但在我看来,本系列其他文章已经讲过的内容,就不必重复赘述。当然,我们实现的功能可能会让很多人感到惊讶,甚至心生怀疑。这很正常,本就是学习过程的一部分。
在结束本文、开启下一篇文章之前,我还有一点建议送给各位读者。在上一节你已经看到,我们可以实现很多有意思的功能。仔细观察就能发现,我们修改了 OBJ_LABEL 对象的尺寸,用来容纳更长一点的文本。我的建议是:弄懂 OBJ_EDIT 对象中的文本创建机制。只有理解这一点,你才能在图表展示文本时,动态调整 OBJ_LABEL 对象的大小。
另外,可以尝试思考如何让 OBJ_LABEL 对象在图表上拖动移动。当前它会固定在创建的位置无法挪动。原因在于:选中对象时,CHARTEVENT_OBJECT_CLICK 事件会把它转换成 OBJ_EDIT 对象,这就导致我们无法修改它的位置。
除此之外,我们还有其他可以拓展的功能,大概率会放到下一篇文章讲解。如果我后续发现该内容趣味性不强、文章价值有限,我会单独说明。无论如何,读完本文,你对 MQL5 和 MetaTrader 5 的了解,相比文章开头已经有所增长。
请使用附件里提供的代码多多学习、动手实操。我们下篇文章再见。
| MQL5 文件 | 说明 |
|---|---|
| 代码 01 | 对象事件演示 |
| 代码 02 | 对象事件演示 |
| 代码 03 | 对象事件演示 |
| 代码 04 | 对象事件演示 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/16050
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
MQL5自优化智能交易系统(第十二部分):利用矩阵分解构建线性分类器
市场模拟:MQL5 中的 SQL 入门(一)
决斗者算法
交易中的神经网络:自适应周期分段(结论)