从基础到中级:函数指针
概述
在上一篇文章“从基础到中级:对象(二)”中,我们开始探讨可用于操作图表上显示的对象的第一类事件。
然而,该文章使用了一个事件类型,当用户与图表交互时,MetaTrader 5 默认情况下总是会触发该事件。在这种情况下,我们指的是按键操作。由于此事件很容易通过 OnChartEvent 处理程序进行拦截,你可能会认为除了拦截该事件外,没有其他使用键盘的方法。亲爱的读者,事实并非如此。尽管脚本既不直接使用也不支持 OnChartEvent 处理程序,但我们可以启用一些机制,借助脚本并通过键盘来控制某些对象属性,尽管这种方法有些不常见。
因此,在探讨鼠标事件处理之前,我们先来看看在使用脚本时如何处理键盘事件。尽管 MetaTrader 5(以及 MQL5)并非专为这类操作而设计,但由于其开发初衷是用于处理价格图表,因此了解哪些操作可行、哪些不可行对您来说至关重要。必须了解存在的局限性。
包含事件的脚本?
首先,你需要明白这一点:我们不能在脚本中使用事件。但这并不妨碍我们编写一个能够处理键盘事件的脚本。然而,事情真正变得复杂的地方在于,你需要明白 MQL5 并非为某些特定类型的实现而设计。因此,当仅使用 MQL5 编程时,会出现各种限制和困难。
但是,如何在脚本内部拦截和处理键盘事件呢?事实是我们做不到。我们实际上能做到的是拦截某些按键,并通过一种过滤机制,创建类似于键盘事件处理程序的东西。尽管在现实中我们会使用 OnChartEvent 处理程序。
为了更清楚地说明这一点,我们将使用我们在上一篇文章中分析过的代码。这将使我们更容易准确地理解我们可能会遇到的问题。下面显示的是相关代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_KEY_UP 38 05. #define def_KEY_DOWN 40 06. //+----------------+ 07. #define macro_NameObject "Demo" + (string)ObjectsTotal(0) 08. //+------------------------------------------------------------------+ 09. string gl_Objs[2]; 10. //+------------------------------------------------------------------+ 11. int OnInit() 12. { 13. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 14. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue); 15. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 16. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple); 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. static int p = 0; 29. MqlRates rate[1]; 30. 31. switch(id) 32. { 33. case CHARTEVENT_KEYDOWN: 34. switch ((int)lparam) 35. { 36. case def_KEY_DOWN: 37. p = (p < Bars(_Symbol, _Period) ? p + 1 : p); 38. break; 39. case def_KEY_UP: 40. p = (p > 0 ? p - 1 : p); 41. break; 42. default: 43. return; 44. } 45. Comment(StringFormat("Current bar analyzed: %d", p)); 46. CopyRates(_Symbol, _Period, p, rate.Size(), rate); 47. ObjectMove(0, gl_Objs[0], 0, rate[0].time, rate[0].close); 48. ObjectMove(0, gl_Objs[1], 0, rate[0].time, rate[0].close); 49. break; 50. } 51. ChartRedraw(); 52. }; 53. //+------------------------------------------------------------------+ 54. void OnDeinit(const int reason) 55. { 56. Comment(""); 57. for (uint c = 0; c < gl_Objs.Size(); c++) 58. ObjectDelete(0, gl_Objs[c]); 59. ChartRedraw(); 60. }; 61. //+------------------------------------------------------------------+
代码 01
执行代码 01 时,将得到以下结果:

动画 01
现在的问题是:我们如何使用脚本创建出与代码 01 类似的东西,并得到与动画 01 相同的结果?很好,这正是我们要回答的问题。
为了实现这一目标,我们需要使用 MQL5 库中的各种函数。然而,由于我们希望专注于 MQL5,因此在此不会使用任何其他方法。尽管使用基于 C 或 C++ 代码的方法可以使任务变得简单得多,但在本文这种针对初级到中级水平的文章中,我们不会采用这种方法。
也许将来,如果我决定展示如何开发更高级的代码,而其中需要使用用其他语言编写和编译的程序,我们会以更简单的方式来看待同样的问题。然而,这也复杂得多,因为要确保语言之间的交互,从而完成任务,必须同时熟练掌握 MQL5 和另一种编程语言。
那么,让我们回到主要问题上来。首先:这里我们不会采用那种完整的程序结构来组织代码。这是因为我们的目标并非开发一个应用程序,而是展示如何解决问题。因此,我们需要制定一个行动计划。为此,第一步是确定初始脚本的外观。
如您所见,无论是在代码中还是在动画中,我们都有两个对象:一个是水平线,另一个是垂直线。两者都将关联到 K 线的生成时间和收盘价。当脚本完成执行 — 并且是正常结束时 — 我们需要删除该脚本创建的两个对象。
太好了,现在我们有了初步的行动计划。之后,我们就可以开始实现源代码了。如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_KEY_UP 38 05. #define def_KEY_DOWN 40 06. //+----------------+ 07. #define macro_NameObject "Demo" + (string)ObjectsTotal(0) 08. //+------------------------------------------------------------------+ 09. string gl_Objs[2]; 10. //+------------------------------------------------------------------+ 11. void OnStart(void) 12. { 13. int p = 0; 14. MqlRates rate[1]; 15. 16. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 17. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue); 18. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 19. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple); 20. 21. Comment(StringFormat("Current bar analyzed: %d", p)); 22. CopyRates(_Symbol, _Period, p, rate.Size(), rate); 23. ObjectMove(0, gl_Objs[0], 0, rate[0].time, rate[0].close); 24. ObjectMove(0, gl_Objs[1], 0, rate[0].time, rate[0].close); 25. 26. Comment(""); 27. for (uint c = 0; c < gl_Objs.Size(); c++) 28. ObjectDelete(0, gl_Objs[c]); 29. ChartRedraw(); 30. } 31. //+------------------------------------------------------------------+
代码 02
代码 02 是我们方法的基础。然而,如果我们尝试运行它,我们将看不到任何结果。原因在于,在我们注意到这些对象实际存在之前,它们就已经被创建、定位和删除了。
亲爱的读者,在代码 02 中,我想提请您注意一点。仔细查看代码 02 的每一行,你会发现这些行在代码 01 中也是完全相同的。但这里我们什么也看不到。原因在于,代码 01 中所示的 OnChartEvent 事件处理程序中的第 21 行至第 24 行之间的代码片段执行速度过快,导致我们没有机会进行交互。这正是我们需要介入以实现键盘事件处理的片段。
很好,我们已经知道源代码的样子了,所以现在是时候把任务分解成更小的部分了。编写单体代码是一项非常繁琐的工作。因此,代码 02 将变为代码 03。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_KEY_UP 38 05. #define def_KEY_DOWN 40 06. //+----------------+ 07. #define macro_NameObject "Demo" + (string)ObjectsTotal(0) 08. //+------------------------------------------------------------------+ 09. string gl_Objs[2]; 10. //+------------------------------------------------------------------+ 11. void OnStart(void) 12. { 13. Init(); 14. KeyEvent(def_KEY_DOWN); 15. Deinit(); 16. } 17. //+------------------------------------------------------------------+ 18. void Init(void) 19. { 20. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 21. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue); 22. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 23. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple); 24. } 25. //+------------------------------------------------------------------+ 26. void KeyEvent(int lparam) 27. { 28. static int p = 0; 29. MqlRates rate[1]; 30. 31. switch (lparam) 32. { 33. case def_KEY_DOWN: 34. p = (p < Bars(_Symbol, _Period) ? p + 1 : p); 35. break; 36. case def_KEY_UP: 37. p = (p > 0 ? p - 1 : p); 38. break; 39. default: 40. return; 41. } 42. Comment(StringFormat("Current bar analyzed: %d", p)); 43. CopyRates(_Symbol, _Period, p, rate.Size(), rate); 44. ObjectMove(0, gl_Objs[0], 0, rate[0].time, rate[0].close); 45. ObjectMove(0, gl_Objs[1], 0, rate[0].time, rate[0].close); 46. } 47. //+------------------------------------------------------------------+ 48. void Deinit(void) 49. { 50. Comment(""); 51. for (uint c = 0; c < gl_Objs.Size(); c++) 52. ObjectDelete(0, gl_Objs[c]); 53. ChartRedraw(); 54. } 55. //+------------------------------------------------------------------+
代码 03
现在我们的代码质量更高了,真是令人愉悦。这是因为,在查看代码 03 时,我们可以明确看到真正需要执行的操作。我们需要在第 14 行运行某种循环,以便与对象进行交互并控制它们,如动画 01 所示。问题是,我们该如何做到这一点?接下来就是最精彩的部分了。但在我们开始之前,我想提醒大家,循环是有风险的。这一点已经解释过了,安全退出循环的方法也已经解释过了。
由于 MetaTrader 5 需要从头开始重建图表时,例如当用户要求 MetaTrader 5 更改图表周期时,每个脚本都会从图表中永久删除,因此脚本将在那一刻被删除。然而,重要的是要明白,仅仅删除脚本是不够的。它所创建的对象也必须被移除。否则,一旦 MetaTrader 5 开始重新绘制图表,这些对象就会再次出现在图表上。
我知道这听起来可能相当令人震惊,但我告诉你这些,是为了让你在使用脚本中的对象时多加小心。如果你不希望脚本创建的对象在脚本运行完毕后仍保留在图表上,那么你应该删除它们。否则,直接忽略这一步删除操作就可以了。
现在,让我们继续学习循环。这里的问题不在于创建循环本身,而在于确保循环不过于激进,不会消耗过多的 CPU 资源。由于键盘事件 — 无论用户多么狂野 — 并非每时每刻都会发生,我们可以在循环体中添加一些元素。因此,我们将得到一个对 CPU 使用率要求低得多的脚本。分析了这些可能性之后,我们得出以下结论:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_KEY_UP 38 05. #define def_KEY_DOWN 40 06. //+----------------+ 07. #define macro_NameObject "Demo" + (string)ObjectsTotal(0) 08. //+------------------------------------------------------------------+ 09. string gl_Objs[2]; 10. //+------------------------------------------------------------------+ 11. void OnStart(void) 12. { 13. Init(); 14. while (!IsStopped()) 15. { 16. if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) 17. KeyEvent(def_KEY_UP); 18. if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) 19. KeyEvent(def_KEY_DOWN); 20. Sleep(100); 21. } 22. Deinit(); 23. } 24. //+------------------------------------------------------------------+ 25. void Init(void) 26. { 27. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 28. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue); 29. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 30. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple); 31. } 32. //+------------------------------------------------------------------+ 33. void KeyEvent(int lparam) 34. { 35. static int p = 0; 36. MqlRates rate[1]; 37. 38. switch (lparam) 39. { 40. case def_KEY_DOWN: 41. p = (p < Bars(_Symbol, _Period) ? p + 1 : p); 42. break; 43. case def_KEY_UP: 44. p = (p > 0 ? p - 1 : p); 45. break; 46. default: 47. return; 48. } 49. Comment(StringFormat("Current bar analyzed: %d", p)); 50. CopyRates(_Symbol, _Period, p, rate.Size(), rate); 51. ObjectMove(0, gl_Objs[0], 0, rate[0].time, rate[0].close); 52. ObjectMove(0, gl_Objs[1], 0, rate[0].time, rate[0].close); 53. } 54. //+------------------------------------------------------------------+ 55. void Deinit(void) 56. { 57. Comment(""); 58. for (uint c = 0; c < gl_Objs.Size(); c++) 59. ObjectDelete(0, gl_Objs[c]); 60. ChartRedraw(); 61. } 62. //+------------------------------------------------------------------+
代码 04
现在来看第 14 行的循环。这样,我们就能得到下面动画中展示的行为。

动画 02
很有意思,不是吗?但这并不是全部内容。还有更多内容需要展示。可以在下面的动画中看到。

动画 03
亲爱的读者,请注意:当我们请求更改图表周期时,MetaTrader 5 会删除图表并立即重新创建。由于 MetaTrader 5 中的脚本默认情况下不会重新加载,因此脚本执行完毕后,由其创建的对象将从图表中移除 — 这正是我们在上述代码 04 中想要实现的效果。
但你可能会疑惑:为何要使用第 16 行和第 18 行的函数?这样做的原因是为了避免使用任何不属于标准 MQL5 库的内容。由于 MQL5 本身没有这方面的需求,因此并未提供任何用于直接读取按键状态的函数或过程。这样,我们就可以构建一个相当于通过 OnChartEvent 拦截 CHARTEVENT_KEYDOWN 的机制,如代码 01 所示。
然而,这并不意味着我们不能使用这种读取键盘状态的方法。为了实现这一点,我们需要使用不属于 MQL5 的机制,因此,在这些从基础到中级的文章中,我们不会对这些机制进行讨论。
好的,但你可能会注意到,在代码 04 中,我们使用了常量或枚举,这使得理解所需键的类型变得更加容易。同时,我们可以看到,我们并不一定需要完全按照代码 04 中的设置来配置所有内容。我们可以稍微改进一下。在这种情况下,由于我们不想展示一个过于简单且无趣的修改,我们将借此机会解释 MQL5 中的另一个可用功能。这是一个具有非常特定目的的功能,我们展示此功能运作的机会不多;而这个案例就是其中之一。为了更好地划分话题,让我们进入下一个话题。
函数指针
这是我们难得的机会之一,来解释 MQL5 中具有特定用途的机制。既然在之前的话题中,我们实现了一个之前作为指标展示的替代版本,那么现在我们就有了机会来解释函数指针。这是一个非常有趣的语言特性,但它会给任何初学者带来极大的困惑,尤其是当他们对某些概念没有清晰理解的时候。
首先:在相当长的一段时间里,我们都没有提到过理解某个主题需要任何先决条件。这是因为这些话题总是与最近的一篇文章相关。然而,这是我们所使用的概念出现在相对较旧的文章中的情况之一。在这种情况下,我指的是文章“从基础到中级:变量(三)”。那篇文章结束了对变量和常量相关主题的讨论。为了理解我们接下来在这里要做什么,对那里所呈现的概念有清晰的认识至关重要。
首先,让我们来看一个非常简单且看似荒谬的例子。然而,这将有助于我们理解如何运用我们即将研究的结构。源代码如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. string Msg_01(const int value) 05. { 06. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 07. } 08. //+------------------------------------------------------------------+ 09. string Msg_02(const int value) 10. { 11. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 12. } 13. //+------------------------------------------------------------------+ 14. void OnStart(void) 15. { 16. Print(Msg_01(171)); 17. Print(Msg_02(-375)); 18. } 19. //+------------------------------------------------------------------+
代码 05
好的,代码 05 非常简单,所以我们就不解释了。但运行结果如下所示。

图 01
虽然代码 05 非常简单,但它包含了一些元素,在继续学习更复杂的内容之前,我们有必要先了解这些元素。这里的主要问题是:我们能创建什么机制来连接第 04 行定义的函数和第 09 行定义的函数?这使我们能够在不直接指定函数名称的情况下选择要使用的函数。
好吧,也许你正在考虑重载,这在文章“从基础到中级:重载”中已经讨论过。然而,重载在这里并不适用,至少不是我们想要使用的方式。事实上,最接近理想的机制是数组。在文章“从基础到中级:数组(四)”中,我们用简单的语言解释了如何使用数组。在那里,重点在于存储离散值,如整数和浮点数。然而,数组也可以存储其他类型的信息。在本例中,指的是函数或过程。“等等,但这怎么可能呢?你疯了吗?如何将函数和过程存储在数组中?这完全说不通。”
这正是我们提到你需要非常理解变量概念的原因。有一种特殊类型的变量叫做指针。出于安全和简洁的考虑,MQL5 并未实现指针,或者说,不允许程序员使用指针来实现代码,因此您几乎不会遇到指针。然而,即使在 MQL5 中,也存在创建和使用指针的情况。而且,由于在实际应用中,MQL5 中的指针与 C 和 C++ 中的指针有所不同,所以在这里你几乎听不到关于它们的讨论。
因此,在你们理解我们要做什么之前,我需要你们先理解这一点:任何函数或程序都存在于内存中的特定地址。这个地址存储在一种特殊类型的变量中,称为指针。指针无疑是编程中最强大的工具。但与此同时,它们也是最令人困惑且最难掌握的,因为通过操纵指针,我们是在处理计算机内存的内容,而不管我们访问的是哪种类型的数据。
好吧,我们不会深入探讨这个问题,正是因为没有必要这么做。你需要了解以下内容:
指针是一种变量。该变量指向一个内存区域。如果该区域包含可执行代码,我们可以将程序执行转移到该区域。
仅仅理解这一点,就足以满足我们的需求,也足以应对我们即将要做的事情。很好,现在我们知道指针是什么了,但我们如何创建,或者更确切地说,如何定义一个指针呢?它是用 MQL5 语言编写的。所以,现在正是需要理解模板工作原理的关键时刻。在文章“从基础到中级:模板与类型名(五)”中,我们阐述了模板的工作原理,以及如何利用模板来创建重载函数和过程。但这正是让初学者感到困惑的地方:我们还可以定义数据类型模板。指针正是这样一种数据类型模板。然而,在这种情况下,它是一种用于表示函数或过程签名的数据类型模板。“哇,现在一切都变得非常复杂了,因为在我看来,这完全说不通。”
别担心,我们不要着急。我知道一开始这确实很困难。这正是我之所以现在才提及这一语言特性,并对其进行解释的原因。为了真正理解如何使用被称为指针的这一语言特性,你需要掌握几个不同的概念。
在代码 05 中,第 04 行和第 09 行的函数是故意这样声明的。这样做是为了更容易理解在定义指针时将使用的模板的概念。如果你仔细观察,你会发现,基本上只有函数名发生了变化。实现函数的内容或逻辑并不重要;重要的是声明。由于那里的声明在返回类型以及传递给函数的参数数量和类型上都非常相似,因此我们具备定义指针的合适条件。为此,我们将在下面的代码中添加以下行。我们将逐步展示代码 05 的更改过程,以便您能够真正理解其中的具体操作。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. typedef string (*FnPtr)(const int); 05. //+------------------------------------------------------------------+ 06. string Msg_01(const int value) 07. { 08. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 09. } 10. //+------------------------------------------------------------------+ 11. string Msg_02(const int value) 12. { 13. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 14. } 15. //+------------------------------------------------------------------+ 16. void OnStart(void) 17. { 18. Print(Msg_01(171)); 19. Print(Msg_02(-375)); 20. } 21. //+------------------------------------------------------------------+
代码 06
现在,放下你正在做的所有事情以及任何可能让你分心的事物,仔细看我即将解释的内容。如果你不理解这开头,你就会彻底迷失方向。请注意,在代码 06 中我们添加了第 04 行,而代码 05 中没有该行。第 04 行定义了我们将要使用的指针。
请注意,此语句与第 06 行和第 11 行的内容非常相似。除了省略了变量名,并将函数名替换为(FnPtr)之外,没有其他变化。这个部分,这个元素(FnPtr),可以是任何你想使用的东西。不过,要小心使用,因为我们很快就需要用到这个特定的部分。
很好,第一部分已经解释清楚了。现在我们将声明一个数组,其类型在代码 06 的第 04 行定义。请记住,那里定义了一个指针。第二部分代码如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. typedef string (*FnPtr)(const int); 05. //+------------------------------------------------------------------+ 06. string Msg_01(const int value) 07. { 08. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 09. } 10. //+------------------------------------------------------------------+ 11. string Msg_02(const int value) 12. { 13. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 14. } 15. //+------------------------------------------------------------------+ 16. void OnStart(void) 17. { 18. FnPtr FnMsg[2]; 19. 20. Print(Msg_01(171)); 21. Print(Msg_02(-375)); 22. } 23. //+------------------------------------------------------------------+
代码 07
现在,在第 18 行,即代码 07 中所示,我们定义了一个数组,该数组使用第 04 行定义的类型指针。为了简化任务,我们定义了一个包含两个元素的静态数组。如果你对这里发生的事情有疑问,请查阅之前的文章;对于已经解释过的内容,我不打算赘述。很好,我想你到这里应该都还能跟上。下一步可以在下面的代码中看到。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. typedef string (*FnPtr)(const int); 05. //+------------------------------------------------------------------+ 06. string Msg_01(const int value) 07. { 08. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 09. } 10. //+------------------------------------------------------------------+ 11. string Msg_02(const int value) 12. { 13. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 14. } 15. //+------------------------------------------------------------------+ 16. void OnStart(void) 17. { 18. FnPtr FnMsg[2]; 19. 20. FnMsg[0] = Msg_01; 21. FnMsg[1] = Msg_02; 22. 23. Print(Msg_01(171)); 24. Print(Msg_02(-375)); 25. } 26. //+------------------------------------------------------------------+
代码 08
“哇,这简直太疯狂了!你是不是彻底疯了?代码 08 无法编译。”
代码 08 当然能够通过编译,亲爱的读者。这怎么可能呢?你难道不明白这里发生了什么吗?我们在这里定义数组的元素。如果你觉得这听起来完全不可思议,请返回本话题的开头,试着理解之前所说的内容。在第 04 行,我们定义了一个数据类型。在第 18 行,我们定义了一个变量,该变量将使用与第 04 行相同的数据类型。在第 20 行和第 21 行,我们给第 18 行声明的变量的元素赋值。仅此而已,一切都非常简单明了。最难的部分在下一步中会展示。请查看以下代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. typedef string (*FnPtr)(const int); 05. //+------------------------------------------------------------------+ 06. string Msg_01(const int value) 07. { 08. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 09. } 10. //+------------------------------------------------------------------+ 11. string Msg_02(const int value) 12. { 13. return "Example message made in function " + __FUNCTION__ + " with the value " + (string)value; 14. } 15. //+------------------------------------------------------------------+ 16. void OnStart(void) 17. { 18. FnPtr FnMsg[2]; 19. 20. FnMsg[0] = Msg_01; 21. FnMsg[1] = Msg_02; 22. 23. Print(Msg_01(171)); 24. Print(Msg_02(-375)); 25. 26. for (uchar c = 0, i = 15; c < FnMsg.Size(); c++, i += 30) 27. Print(FnMsg[c](i)); 28. } 29. //+------------------------------------------------------------------+
代码 09
当执行代码 09 时,终端将显示以下消息。

图 02
“这究竟是什么样的疯狂之举?这里到底发生了什么?”好了,亲爱的读者,现在我们已经到了真正困难的部分。从某种意义上说,我甚至在这段代码上做得有点过头了。因为如果你跳过前面的步骤,试图从头开始理解代码 09,你可能会比以往任何时候都更加困惑。这是因为你能理解图 02 中可见的前两行,但我指出的另外两行是从哪里来的呢?这才是关键所在。即便如此,在 MQL5 中,指针其实非常易于解释和理解。但对于那些看到使用这些函数的代码却并不完全理解其中具体实现了什么的人来说,它们仍然可能会让人感到困惑。
请注意,图 02 中高亮的那两行,正是由代码 09 第 27 行的调用产生的。但是怎么做的呢?这就是困惑产生的地方。由于第 18 行的声明使用了指针类型,因此当看到第 27 行时,你可能会期望得到某种类型的信息。但是,编译器识别出这是一个函数调用。这就是为什么本声明要这样表述的原因。因此,结果如图 02 所示。请注意,函数名称无关紧要。第 27 行将确定应该调用哪个函数,因为在第 20 行和第 21 行,我们将一个函数赋值给了数组元素。
你或许在想:“但为什么要制造这样的复杂局面呢?生活中难道这样的事情还不够多吗?即使技术上可行,我们真的需要这样的东西吗?我的天哪!程序员都是些怪人,他们简直疯了。”
或许相当一部分程序员真的会在某个时候精神崩溃,变成非常古怪的人。(笑声)。但总的来说,我们都是非常善良的人,只是有点古怪而已。然而,在这个阶段,我们已经可以设想一些以前不可能做到的事情。那是因为你还没有看过本主题中展示的内容。现在,我们将更新上一个主题中提到的代码 04,以便使用本主题中学到的知识。因此,我们将得到以下代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_KEY_UP 38 05. #define def_KEY_DOWN 40 06. //+----------------+ 07. #define macro_NameObject "Demo" + (string)ObjectsTotal(0) 08. //+------------------------------------------------------------------+ 09. string gl_Objs[2]; 10. //+------------------------------------------------------------------+ 11. typedef void (*ProcPtr)(void); 12. typedef void (*KeyEvent)(int &); 13. //+------------------------------------------------------------------+ 14. void OnStart(void) 15. { 16. ProcPtr proc[3]; 17. 18. proc[0] = Init; 19. proc[1] = Loop; 20. proc[2] = Deinit; 21. 22. for (uint c = 0; c < proc.Size(); c++) 23. proc[c](); 24. } 25. //+------------------------------------------------------------------+ 26. void Init(void) 27. { 28. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 29. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue); 30. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 31. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple); 32. } 33. //+------------------------------------------------------------------+ 34. void Loop(void) 35. { 36. KeyEvent key[3]; 37. int pos = 0; 38. 39. key[0] = Bar_NONE; 40. key[1] = Bar_Next; 41. key[2] = Bar_Prev; 42. 43. while (!IsStopped()) 44. { 45. key[TerminalInfoInteger(TERMINAL_KEYSTATE_UP) ? 1 : (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN) ? 2 : 0)](pos); 46. Sleep(100); 47. } 48. } 49. //+------------------------------------------------------------------+ 50. void Event(const int arg) 51. { 52. MqlRates rate[1]; 53. 54. Comment(StringFormat("Current bar analyzed: %d", arg)); 55. CopyRates(_Symbol, _Period, arg, rate.Size(), rate); 56. for (uint c = 0; c < gl_Objs.Size(); c++) 57. ObjectMove(0, gl_Objs[c], 0, rate[0].time, rate[0].close); 58. } 59. //+------------------------------------------------------------------+ 60. void Deinit(void) 61. { 62. Comment(""); 63. for (uint c = 0; c < gl_Objs.Size(); c++) 64. ObjectDelete(0, gl_Objs[c]); 65. ChartRedraw(); 66. } 67. //+------------------------------------------------------------------+ 68. void Bar_Prev(int &arg) 69. { 70. arg = (arg < Bars(_Symbol, _Period) ? arg + 1 : arg); 71. Event(arg); 72. } 73. //+------------------------------------------------------------------+ 74. void Bar_Next(int &arg) 75. { 76. arg = (arg > 0 ? arg - 1 : arg); 77. Event(arg); 78. } 79. //+------------------------------------------------------------------+ 80. void Bar_NONE(int &arg) 81. {} 82. //+------------------------------------------------------------------+
代码 10
执行代码 10 后,得到的效果与动画 01 中展示的行为完全一致。因此,我们在此不再赘述。但是,在代码 10 中,我允许自己稍微自由发挥一下。我这样做是为了让你们明白,我们做事并不总是只有一种方法。有很多方法可以达到同样的效果。其中一些方法较为简单,但会使代码扩展速度稍慢;而另一些方法则相对复杂,但允许代码非常快速地扩展。
无论如何,在我看来,代码 10 并不包含难以理解的元素。尤其是因为此时这些材料在记忆中仍然相当清晰。然而,代码 10 中的某些地方,如目前所呈现的,可能看起来并不完全合乎逻辑。让我们简要解释一下这些地方。
第一个是在 OnStart 过程中。这里我们看到,第 16 行声明了一个过程数组。紧接着,在第 18、19 和 20 行,我们为数组中的各个元素分别指定了要执行的过程。然后我们进入一个循环,按一定的顺序执行这些过程。我明白乍一看这可能显得不合逻辑,但你需要明白,这些文章的目标是教你像程序员一样思考。
现在想象一下,有一系列不断重复的小步骤,必须按照特定的顺序执行。如果你必须逐行编写每个需要执行的过程,这不仅会耗费大量时间,而且还会使更改或纠正执行顺序成为一项繁琐的任务。但如果可以用数学表达式来计算数组中的索引,那么完全有可能将过程定义为该数组的元素。通过这样做,你可以使用循环按特定顺序执行数组元素,否则这是不可能实现的。
至于第 34 行的 Loop 过程,它的目的非常相似。然而,那里的机制运作方式略有不同。在这种情况下,在第 39 行和第 41 行之间,我们定义了一些根据所按下的键来使用的过程。在第 45 行,我们使用三元运算符调用相应的过程。
这种方法,或者说在解决特定问题时采用的这种思维方式,有时能将一项原本非常繁琐的任务变得更为简单和迅速。因为如果需要做出改变,我们只需进行最少的改动,这样就能非常迅速地实现任何解决方案。
最后的想法
尽管我们能够做的远不止本文所展示的,但我相信,对于许多人而言,本文所涵盖的内容是全新的,因此应该进行深入研究。有必要理解本文中提出的每一点。
附件中包含本文介绍的主要代码示例。因此,请利用这段时间练习这里展示的所有内容。同时,请尝试回顾之前文章中解释的内容,并思考如何将相同的事实和概念与我们在此处所看到的内容结合起来使用。尝试实现一些东西,让你能够运用迄今为止所学到的所有知识。下一篇文章,我们将再次讨论与对象相关的事件。但这次的主要议题将是使用鼠标,因为与键盘相关的部分已经得到了充分的研究。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15995
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
您应该了解的MQL5向导技巧(第七十六部分):利用AO形态与包络通道进行监督学习建模
数据科学与机器学习(第四十六部分):在Python中使用N-BEATS进行股票市场预测
市场模拟(第 24 部分):SQL 入门(七)
您应该了解的MQL5向导技巧(第 75 部分):动量震荡指标与包络线的应用