
开发回放系统 — 市场模拟(第 27 部分):智能交易系统项目 — C_Mouse 类
概述
在上一篇文章《开发回放系统(第 26 部分):智能交易系统项目(I)》中,我们详细研究了第一个类如何开始构造。现在我们扩展这些思路,并令它们更实用。这就把我们带到了 C_Mouse 类的创建。它提供了最高级别的编程能力。不过,说到高级或低级编程语言,并不是在代码中包含污言秽语或行话。它有其它含义。当我们谈论高级或低级编程时,我们意指对于其他程序员来说理解代码是多么容易或困难。事实上,高级和低级编程之间的区别表明了代码对其他开发人员来说是多么简单或复杂。因此,如果代码与自然语言相似,则可视其为高级;如果代码与自然语言相似度较低,且更接近处理器解释指令,则视其为低级。
我们的目标是令类代码尽可能高级,同时尽可能避免某些类型的建模,那样可能会令经验不足的人难以理解。这是目标,尽管我不能保证它会完全达成。
C_Mouse 类:开始与用户交互
鼠标和键盘是用户与平台之间最常用的交互方式。因此,交互简单有效非常重要,这样用户就不必重新学习如何执行动作。代码从以下几行开始:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_Terminal.mqh" //+------------------------------------------------------------------+ #define def_MousePrefixName "MOUSE_" #define def_NameObjectLineH def_MousePrefixName + "H" #define def_NameObjectLineV def_MousePrefixName + "TV" #define def_NameObjectLineT def_MousePrefixName + "TT" #define def_NameObjectBitMp def_MousePrefixName + "TB" #define def_NameObjectText def_MousePrefixName + "TI" //+------------------------------------------------------------------+ #define def_Fillet "Resource\\Fillet.bmp" #resource def_Fillet //+------------------------------------------------------------------+
我们包含的一个头文件里包含了 C_Terminal 类。如上一篇文章所述,这个 C_Mouse 类文件与 C_Terminal 类文件位于同一目录之中,这令我们能够毫无障碍地使用这种语法。我们定义要包含在可执行文件中的资源名称,允许您挪动它,而无需再单独下载资源。这在许多情况下非常实用,尤其在使用过程中资源的可用性至关重要之时。我们通常将资源放在特定的目录中,从而便于访问。以这种方式,它就能始终与头文件一起编译。我们在 C_Mouse.mqh 文件所在的文件夹中加入一个名为 “Resource” 的目录。Fillet.bmp 文件位于 “Resource” 目录之中。如果我们在保持相同建模的同时更改目录结构,编译器会确切地知道在何处可以找到 Fillet.bmp 文件。一旦代码编译完毕,我们就能加载可执行文件,且不必担心找不到资源,因为它已被嵌入到可执行文件自身。
在此步骤中,我们首先定义一个名称,实际上它是稍后定义的其它名称的前缀。定义的用法令开发和维护变得更加容易,这是专业代码中的惯用做法。程序员定义要在代码中用到的各种名称和元素,这通常在 Defines.mqh 文件,或其它类似文件中完成。据此文件,可以轻松更改定义。不过,由于这些定义仅存在于该文件但中,因此无需在其它任何地方声明它们。
#undef def_MousePrefixName #undef def_NameObjectLineV #undef def_NameObjectBitMp #undef def_NameObjectLineH #undef def_NameObjectLineT #undef def_NameObjectText #undef def_Fillet
这段代码告诉编译器,由 C_Mouse.mqh 文件定义和可见的所有符号和名称,此刻都不再可见。通常不建议删除或更改其它文件中的定义 — 这不是惯用做法。这就是为什么我们在名称实际出现和使用的地方公布它们的原因。之后,这些定义将被删除。没有准则就更改或删除定义也非良好做法。如果我们需要在多个文件中使用一个定义,最好为它创建一个单独的文件。
现在我们继续类代码的前几行。这就是事情变得有趣的地方。一切从这里开始:
class C_Mouse : public C_Terminal { protected: enum eEventsMouse {ev_HideMouse, ev_ShowMouse}; enum eBtnMouse {eKeyNull = 0x01, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10}; struct st_Mouse { struct st00 { int X, Y; double Price; datetime dt; }Position; uint ButtonStatus; };
在这个片段中,我们看到 C_Terminal 类由 C_Mouse 类公开继承,这意味着使用 C_Mouse 类,我们就可访问 C_Terminal 类中的所有公开方法。因此,C_Mouse 类拥有的功能会比仅局限于 C_Mouse.mqh 文件中的代码更多。继承不仅提供了这种益处,还可以提高类的效率,我们将在以后的文章中讨论这些益处。我们继续操控该代码部分。在代码的受保护部分,我们有两个枚举声明,允许我们在稍高的级别进行编程。第一个枚举非常简单,遵循我们在上一篇文章中涵盖的相同概念和规则。另一方面,第二段清单也许看似有点令人困惑和复杂,但我们首先要探讨其复杂性的原因,以及它存在的原因。
该枚举为我们提供了一个机会,否则将更难维护;也就是说,它将为我们节省大量工作。此特定枚举创建名称定义,其等效于 #define 编译器指令。不过,我们决定使用枚举替代定义。这允许我们运用稍微不同的技术,但同时代码更容易理解。使用枚举,我们将看到代码如何变得更具可读性。这在复杂代码中变得至关重要。初看时,如果您认为该枚举声明非常困惑和复杂,那么您大概并未完全明白枚举的工作原理。从编译器的角度来看,枚举只是一连串定义,其中默认第一个元素从索引零开始。然而,我们可以为枚举设置所需的起始索引,编译器将从该值开始递增后续索引值。这在许多场景下非常实用,其中一个确定的索引值当作序列的起始值。通常,程序使用一长串枚举清单,其中的错误值是基于某些特定准则设置的。如果您定义了一个名称,并为其分配一个特定值,编译器将自动递增所有后续名称的值。这令创建大型定义清单更加容易,且不会在某些时刻出现重复值的风险。
事实上,许多程序员不用这种技术,这很令人惊讶,因为它可以极力避免某些类型的项目编程时出现错误。现在,您已经明白了这一点,您可以进行试验并发现,使用枚举可以极大简化创建大量相关元素清单(无论顺序与否)的过程。我们正在探索的方式旨在令代码更易于阅读和理解,从而改进编程。
下一部分是一个结构,负责通知代码的其余部分鼠标正在做什么。在这一点上,也许十分期望声明一个变量,但在类内部的私密子句之外声明变量,不认为是良好的编程做法。其他人也许认为,将这些声明放在代码的公开部分更为合适。不过,我更愿意从更受限的访问级别开始,允许公开访问仅作为最后的妥协。我们必须确保函数和方法拥有公开访问权限,但与类直接相关的函数和方法除外。否则,我们始终建议开始阶段给元素最低授权。
继续这个思路,我们看看 C_Mouse 类中存在的变量:
private : enum eStudy {eStudyNull, eStudyCreate, eStudyExecute}; struct st01 { st_Mouse Data; color corLineH, corTrendP, corTrendN; eStudy Study; }m_Info; struct st_Mem { bool CrossHair; }m_Mem;
特别有趣的是,我们已经有一个枚举,然其对类之外的任何其它代码部分都不可见。这是因为该枚举仅供该类内部使用,代码的其它部分没必要知晓它的存在。这个概念被称为封装,它基于这样的原则,即其它代码片段不会知晓如何实际执行给定任务的工作。这种类型的方式颇受函数库开发人员的高度重视,他们允许其它程序员在不透露函数库代码实际工作方式的情况下访问过程。
接下来,我们找到结构。这处用到了另一个可以在类主体之外访问的结构,我们将在解释访问过程和函数时详细讲述它。在这一点上,重点是要明白该变量指代一个私密类结构。还有另一种结构,而在这种情况下,我更喜欢使用这种方式,因为它清楚地表明内容是特殊的,且只在代码中非常特定的位置才可访问。不过,没有什么可以阻止您在以前的结构中声明相同的数据。在编写代码时,您只需要当心不要更改此数据,因为第一个结构中包含的数据是通用的,可以随时更改。
我们已经涉及到与变量相关的部分,现在我们能转入函数和方法的分析。请看以下代码:
inline void CreateObjectBase(const string szName, const ENUM_OBJECT obj, const color cor) { ObjectCreate(GetInfoTerminal().ID, szName, obj, 0, 0, 0); ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BACK, false); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, cor); }
这段代码可促进软件重用,因为贯穿 C_Mouse 类的整个开发过程,我们需要创建各种元素,且必须遵守某些标准。因此,为了促进该过程,我们将把这个创建任务集中在一个方法之中。这种做法在声明中经常看到,尤其性能是一个关键因素时,就会使用特定的关键字。我之所以如此选择,是因为我希望编译器直接在声明代码的地方包含代码,其作用类似于宏。好吧,这也许会导致可执行文件大小的增加,但作为回报,我们将在运行时的整体性能方面获得改进,尽管很小。有时,出于各种因素,性能提升很小,这也许无法明断可执行文件大小增加是合理的。
在此,我们有一个状况,对许多人来说也许看似微不足道,但将成为贯穿代码反复出现的一个方面。此函数引用在 C_Terminal 类中声明的结构。不过,编译器并未将其解释为函数,而是当作一个常量变量。但这怎么可能呢?编译器如何把一个看起来像函数的声明当作常量变量?初看,这没有多大意义。不过,请更详细地查看这段调用代码,及其在 C_Terminal 类中的实现:
inline const st_Terminal GetInfoTerminal(void) const { return m_Infos; }
这段代码返回对属于 C_Terminal 类的结构的引用。我们返回引用的变量是类的私密变量,在任何境况下都不应被 C_Terminal 类内部以外的任何代码修改其值。为了确保代码在访问该私密变量时不会对其进行任何更改,我们决定包含一个特殊声明。以这种方式,编译器就可确保任何代码接收常量的引用,且无法更改其值。该措施用于避免意外更改或编程错误。因此,即使在 C_Terminal 类内部,尝试在函数中以不恰当的方式更改数值,编译器也会将此过程识别为错误,因为根据编译器的说法,没有任何信息可以更改。发生这种情况是因为该处不适合或错误地进行这种更改。
这种类型的编程虽然劳动强度更大,但提高了代码的健壮性和可靠性。不过,此上下文中存在一个缺陷,稍后会解决它。这是因为现在解释这一决定会令整体解释复杂化。现在我们看看 C_Mouse 类中的以下方法:
inline void CreateLineH(void) { CreateObjectBase(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); }
它创建一条代表价格线的水平线。重点要注意的是,把所有复杂性委托给另一个过程,我们只需要写一行。这种类型的方式通常由宏所取代。不过,我更喜欢尝试不用宏的做法。另一种方式是将相同的内容粘贴到将要进行调用的位置,因为它只是一行。个人而言,我不推荐这种做法,并非因为它是错误的,而是因为它需要修改所有行,而实际上只有一行改了。这可能是一项繁琐的任务,并且容易出错。故此,虽然将代码直接放在引用点似乎更实用,但用宏或声明中带有 “inline” 关键词的代码会更安全。
下面我们将看到的方法也许现在没有多大意义,但在我们开始解释之前了解它们很重要。第一种方法如下所示:
void CreateStudy(void) { CreateObjectBase(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH); CreateObjectBase(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH); CreateObjectBase(def_NameObjectBitMp, OBJ_BITMAP, clrNONE); CreateObjectBase(def_NameObjectText, OBJ_TEXT, clrNONE); ObjectSetString(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_FONT, "Lucida Console"); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_FONTSIZE, 10); ObjectSetString(GetInfoTerminal().ID, def_NameObjectBitMp, OBJPROP_BMPFILE, "::" + def_Fillet); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_WIDTH, 2); m_Info.Study = eStudyCreate; }
它在图表上创建了研究所需的对象,允许我们能够创建自己的分析风格。这有助于高亮显示有效分析必须要考虑的最相关和必要的信息。此处呈现的研究模型非常简单,但可以开发更多样化的方法,速度更快,且在视觉上不会令图表混乱。作为最简单方法的一个例子,我们将创建一项研究来测试一个价格与另一个价格之间的点数,直观地指示该值是负数还是正数。虽然它并非一个复杂的系统,但它是开发其它更复杂模型的基础。
许多研究使用广泛类型的对象及其组合,有时需要计算或在价格范围内找到一个位置(高点-低点研究)。手动完成所有这些操作不仅缓慢,而且乏味,因为您必须不断地在图表中添加和删除对象。否则,图表可能会变得拥挤和混乱,从而难以识别所需的信息。因此,运用该方法作为创建更精致和更适合您需求的东西的基础。现在没有必要着急,因为以后有机会改进它。
您唯一需要做的就是确保当您想要添加文本(这很有可能)时,它将是创建序列中的最后一个对象。从上面的代码中可以看出这一点,其中显示信息的文本是最后一个创建的对象。这样可以防止它被其它对象掩盖。通常,首先创建若干个对象,其中一个对象也许会掩盖一个非常重要的对象。记住:您最感兴趣的、最重要的元素应始终处于创建队列中的最后一个元素。
对象最初的颜色设置为 clrNONE,不必对这一事实感到困惑,因为这些颜色随后会随着分析的进行而变化。如果前一个方法负责创建分析,则下一个方法实际上会在图表上执行该分析。
void ExecuteStudy(const double memPrice) { if (CheckClick(eClickLeft)) { ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, 1, m_Info.Data.Position.dt, m_Info.Data.Position.Price); ObjectMove(GetInfoTerminal().ID, def_NameObjectBitMp, 0, m_Info.Data.Position.dt, m_Info.Data.Position.Price); ObjectMove(GetInfoTerminal().ID, def_NameObjectText, 0, m_Info.Data.Position.dt, m_Info.Data.Position.Price); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > m_Info.Data.Position.Price ? m_Info.corTrendN : m_Info.corTrendP)); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_COLOR, (memPrice > m_Info.Data.Position.Price ? m_Info.corTrendN : m_Info.corTrendP)); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > m_Info.Data.Position.Price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER)); ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_ANCHOR, (memPrice > m_Info.Data.Position.Price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER)); ObjectSetString(GetInfoTerminal().ID, def_NameObjectText, OBJPROP_TEXT, StringFormat("%." + (string)GetInfoTerminal().nDigits + "f ", m_Info.Data.Position.Price - memPrice)); } else { m_Info.Study equal eStudyNull; ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T"); } m_Info.Data.ButtonStatus equal eKeyNull; }
初看,在图表上进行分析看似毫无意义,尤其是当您孤立地查看代码时。不过,如果您研究交互代码,其目的会变得更加清晰和易于理解。现在我们尝试搞明白此处要发生什么。不同于创建研究对象的代码,此片段包含一些有趣的元素。本质上,我们区分出代码中的两个主要部分。在第一个部分中,我们检查条件 — 即使没有编程知识,您也能理解要检查什么内容,因为配合高级编程,代码类似于自然语言。在第二个部分中,我们完成分析。这两个部分都非常简单,可以快速理解。在此阶段,我们在屏幕上移动对象,以便指示分析间隔。在接下来的几行中,我们更改对象的颜色,指明走势是向上亦或向下,尽管这往往是显而易见的。不过,也许会浮现一些状况,譬如在使用某种类型的曲线时,通过简单地观察来判定是正值还是负值就变得困难。重点是要记住,可以基于不同的准则以多种方式进行分析。也许最重要的部分是我们基于一些计算或分析来呈现数值的曲线。在此,我们可以随意按不同的方式呈现许多不同的信息,具体取决于每种情况。
我们真正需要的是一种方法,能正确地完成研究演示,这就是第二个代码部分的作用。虽然初看,这部分似乎并不特别值得注意,但有一个方面值得特别注意:ObjectsDeleteAll 函数的调用。为什么这一时刻很重要,需要关注?答案就在 C_Terminal 类。C_Terminal 类的构造函数包含以下行:
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
这一行告诉平台,每次从图表中删除对象时,它都应该生成一个事件,通知哪个对象被删除了。调用 ObjectsDeleteAll 函数,我们删除分析中使用的所有元素或对象。这会导致 MetaTrader 5 为每个从图表中删除的对象生成一个事件。平台将做到这一点,再由我们的代码来决定是否再次创建这些对象。当删除对象失败(因为代码再次创建它们),或它们已被删除但没有相关代码发出通知时,就会出现此问题。在这种状况下,对于 CHART_EVENT_OBJECT_DELETE 属性将设置为 false。虽然这最初不会发生,但随着代码的扩展,有时可能会意外更改此属性,我们也许会忘记重新启用它。结果就是,对象已从图表中删除后,平台未能创建一个事件来通知我们的代码,这可能会导致对象管理中的不准确和错误。
现在我们看看 C_Mouse 类的构造函数。
C_Mouse(color corH, color corP, color corN) :C_Terminal() { m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; m_Mem.CrossHair = (bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, false); CreateLineH(); }
此刻,我们显式调用 C_Terminal 类的构造函数。在执行 C_Mouse 类中的任何其它方法之前,先调用构造函数至关重要,如此可确保 C_Terminal 类的必要值已正确初始化。初始化之后,我们配置某些方面,并保存其它方面的状态。注意这两行,我们与告知平台期望接收鼠标事件的愿望,以及我们不打算使用由平台提供的标准分析工具。应用这些定义后,平台将根据要求报告鼠标事件,如此就符合我们的要求。另一方面,当我们尝试使用鼠标使用分析工具时,我们还要提供我们的代码进行此类分析。
下一段代码是类的析构函数:实现析构函数非常重要,这样我们就可以在 C_Mouse 类使用完毕后恢复平台的原本功能。
~C_Mouse() { ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, 0, false); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair); ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName); }
这个问题虽然看起来有悖常理,但很重要。包含此特定行的原因是,当我们尝试删除由 C_Mouse 类创建的对象(尤其是价格线)时,平台会生成一个事件,通知我们该对象已从图表中删除。然后,我们的代码将尝试在图表中重建该对象,即使它正在被删除。为了防止平台产生这样的事件,我们必须明确表明我们不希望这种情况发生。有人可能会好奇,“但是,C_Terminal 类难道不会通过告诉我们不再希望接收有关从图表中删除对象的事件来解决这个问题吗”?是的,C_Terminal 类会这样做,但由于我们仍然需要 C_Terminal 类中存在的一些数据,故我们允许编译器隐式调用 C_Terminal 类的析构函数,这仅在执行 C_Mouse 类析构函数的最后一行后发生。在不添加专门代码行的情况下,平台将继续生成事件,故即使最初删除了价格线,也可以在代码彻底完成之前将其放回原处。析构函数的其余行更简单,因为我们所做的只是将图表返回到其原始状态。
我们已经来到了本文讨论的最后一个函数。
inline bool CheckClick(const eBtnMouse value) { return (m_Info.Data.ButtonStatus & value) == value; }
在此行中,使用我们为接收鼠标事件定义的枚举,我们检查平台提供的值是否与代码中特定点的预期值匹配。如果确认匹配,则函数将返回 true;否则将返回 false。尽管这在目前看似微不足道,但当我们开始与系统进行更密集的交互时,这样的检查将非常实用。声明该函数有一个特殊的技巧,可以令其更易于使用,但由于此刻这一点上并不重要,因此我不会深入细节。
下一个函数与上一个函数一样简单,并且遵循与 C_Terminal 类的 GetInfoTerminal 函数相同的原理。
inline const st_Mouse GetInfoMouse(void) const { return m_Info.Data; }
目标是返回存储鼠标数据结构的变量中包含的信息,确保未经 C_Mouse 类的显式许可,无法更改此数据。既然我们已谈及这种方法,我认为没有必要重复它的解释。本质上,两者都以类似的方式运作。
最后,我们来到了 C_Mouse 类代码的高潮。但是,我认为将 C_Mouse 类中存在的最后一个函数留到下一篇文章中讨论是很重要的。然后我们将解释其中的原因。
结束语
我们正在创造一些非常有前途的东西,尽管它离最终还很远。我们将在下一篇文章中继续。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11337


