
开发回放系统(第 35 部分):进行调整 (一)
概述
在上一篇文章《开发回放系统》(第 34 部分)中:订单系统 (三) ,我提到过该系统有几个相当奇怪甚至神秘的错误。这些错误或故障是由系统内的某些交互作用造成的。尽管我们试图找出这些故障的原因以消除它们,但所有这些尝试都没有成功。其中有些情况完全不合理,例如,当我们在 C/C++ 中使用指针或递归时,程序就会崩溃。第一步就是验证这些机制。但是,在 MQL5 中,这种情况的发生与 C/C++ 中的情况不同。在做了一些小修改后,我成功地解决了其中一个缺陷。虽然这个解决方案看起来并不优雅,但它却让其中一个故障彻底消失了。
不过,我们仍然需要对代码进行稍微大一点的修改,才能彻底消除影响系统运行的错误。它们可能已经在这里存在了很长时间,因为系统早期没有某些类型的交互。一旦这些交互开始发生,这些错误就会变得很明显。
现有的故障不会对系统运行产生负面影响,但却无法实现真正正确的运行。所有这一切都让用户在使用该程序时感到相当不愉快,甚至无法接受。第一个缺陷很容易解决,我们就从它开始。
解决服务繁忙(Service Busy)指示
其中第一个缺陷是最容易纠正的。它出现在以下情况:当我们在下订单时按下 CTRL 或 SHIFT 键时,会收到服务繁忙的提示。这意味着在运行过程中,即使系统运行正常,也会出现服务正在执行其他任务的迹象。这项任务是分时间段进行的。这意味着在对重放/模拟图表进行某种分析之前,需要创建好柱形图。虽然这种错误的危害不大,但它会使重放/模拟器的使用体验变得相当不愉快,因为它更容易造成混淆,而不是提供信息。
有些人认为,最好删除服务繁忙的字样。因为我们已经有一段时间没有在滚动时间时使用柱形创建显示了。但这并不能解决问题。这样做只会把问题推到幕后,把它扫到地毯下面,但其实解决方法非常简单有效。此外,我们还可以保留一些东西,这样将来我们就可以回到可视化系统,看到柱形的创建过程。因为在此之前,我们已经禁用了重放/模拟服务。因此,要解决这个问题,我们需要做出一些改变。让我们从下面的代码开始:
int OnInit() { #define macro_INIT_FAILED { ChartIndicatorDelete(m_id, 0, def_ShortName); return INIT_FAILED; } u_Interprocess Info; ulong ul = 1; m_id = ChartID(); ul <<= def_BitShift; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName); if ((_Symbol != def_SymbolReplay) || (!GlobalVariableCheck(def_GlobalVariableIdGraphics))) macro_INIT_FAILED; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics); if (Info.u_Value.IdGraphic != m_id) macro_INIT_FAILED; if ((Info.u_Value.IdGraphic >> def_BitShift) == 1) macro_INIT_FAILED; IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName + "Device"); Info.u_Value.IdGraphic |= ul; GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value); if (GlobalVariableCheck(def_GlobalVariableReplay)) Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); else Info.u_Value.df_Value = 0; EventChartCustom(m_id, C_Controls::ev_WaitOff, 0, Info.u_Value.df_Value, ""); EventChartCustom(m_id, C_Controls::ev_WaitOff, 1, Info.u_Value.df_Value, ""); Control.Init(Info.s_Infos.isPlay); return INIT_SUCCEEDED; #undef macro_INIT_FAILED }
首先应修改指标文件,修改的具体位置如上图所示。划线部分被删除,代之以新的代码。请仔细观察,因为这是唯一需要做出的改动。它涉及需要传递给 EventChartCustom 函数的 lparam 参数值。奇怪的是,这样一个简单的改变已经结出了硕果。
在同一个文件内,但在不同的函数中,我们需要做非常类似的事情:
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { static bool bWait = false; u_Interprocess Info; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (!bWait) { if (Info.s_Infos.isWait) { EventChartCustom(m_id, C_Controls::ev_WaitOn, 0, 0, ""); EventChartCustom(m_id, C_Controls::ev_WaitOn, 1, 0, ""); bWait = true; } }else if (!Info.s_Infos.isWait) { EventChartCustom(m_id, C_Controls::ev_WaitOff, 0, Info.u_Value.df_Value, ""); EventChartCustom(m_id, C_Controls::ev_WaitOff, 1, Info.u_Value.df_Value, ""); bWait = false; } return rates_total; }
这与上面的 OnInit 代码类似。划掉的部分已用新代码替换。唯一不同的还是 lparam 参数值。 因此,修复再次涉及 EventChartCustom 函数。
为什么会出现这种情况?这样真的有效吗?事实上,在这里做这些改动并不能解决问题。要真正解决这个问题,我们需要进入 C_Control 类,在消息处理函数中添加一个小检查。请看下面的新代码:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; static int six = -1, sps; int x, y, px1, px2; switch (id) { case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn): if (lparam == 0) break; m_bWait = true; CreateBtnPlayPause(true); break; case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff): if (lparam == 0) break; m_bWait = false; Info.u_Value.df_Value = dparam; CreateBtnPlayPause(Info.s_Infos.isPlay); break; //... The rest of the code ... } }
我在这里只展示了我们理解问题所真正需要的片段。请注意,这两个自定义事件中都添加了一个小的检查。用户交互会触发程序中的自定义事件,这就迫使这些检查只允许触发自定义事件。这些事件正是在控制指标代码中触发的。由于一些其他原因,到达处理程序的值将与我们预期的不同。在这种情况下,lparam 参数的值会被考虑在内。今后,当我解释如何在 MetaTrader 5 平台上使用一个相当有趣的东西时,这种事情还会出现。到那时,我会更详细地解释这一错误的原因。不过,目前原因还很奇怪。
但等等,这说不通啊!事实上,一开始我也不明白为什么。不知何故,当你按 SHIFT 或 CTRL 键使用我们正在开发的订单系统时,平台会生成一个事件,而它会触发之前的两个事件。具体地说 CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn 会导致图表上出现 "服务繁忙 "图像。一旦您释放 SHIFT 或 CTRL,则 CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff 事件就会触发,从而使一切重回正轨。
解决了这个问题,我们就可以继续下一个问题了。然而,第二个问题的解决要困难得多。为了避免混淆,我们需要将其放在一个单独的主题中探讨。因此,让我们接着看下一节,了解真实情况。
更深入地重复使用 C_Mouse 类
当我们进行分析时,下一个缺陷就会出现。当这种情况发生时,系统可能会在用户没有察觉的情况下移动位置指示器。要解决这个问题,需要对代码进行更深入的编辑。您可能会认为,只需运行检查以验证用户是否真的在尝试移动滑块对象即可。该滑块对象是控制指示器的组成部分,可确保用户知道重放或模拟处于哪个阶段。但简单的检查并不足以解决这一缺陷。我们必须超越简单的验证。事实上,我们一开始就要做更加复杂的事情,但它能更明确地解决问题,还能让我们以后做其他事情。
我们将使用 C_Mouse 类,该类最初用于 EA,也用于控制指示器。但要做到这一点并不像想象中那么容易,我们需要对代码中的多个地方进行重大修改。我们这样做是为了使 C_Mouse 类在控制指示器和 EA 中都能完全和谐地工作。我们需要一切顺利而没有任何冲突。通过这样做,我们还将打开一扇门,让我们能够做其他事情。
首先,让我们看看 C_Mouse 类及其变化。为了解释这些变化,我们需要看看这个类的代码中到底添加了什么。首先是私有全局变量。修改情况如下:
struct st_Mem { bool CrossHair, IsFull; datetime dt; }m_Mem;
添加的这个变量将用于在类中执行检查。它的首要任务是准确定义类将如何执行:这指的是类在其生存期内应该使用的对象。变量声明后,我们需要以适当的方式启动它。为此,我们将使用类构造函数。现在,第一个复杂问题来了:如何知道是否在 "完整 "模式下使用该系统?一个重要细节:在使用 "完整 "模式时,我们实际上使用的是图形对象。EA 交易将使用这些对象。当我们在 EA 中不使用该类时,则不应创建此类对象。但是,我们怎样才能告诉类,我们使用的是 EA 交易还是其他系统呢?有几种方法,我们需要的是通过类构造函数完成它。这样就避免了很多后续的额外检查,也避免了与错误使用类相关的风险。因此,我们将改变构造函数,如下所示:
C_Mouse(C_Terminal *arg, color corH = clrNONE, color corP = clrNONE, color corN = clrNONE) { Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) return; m_Mem.CrossHair = (bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, false); ZeroMemory(m_Info); m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); }
请注意这里发生的事情。在构造函数声明中,我们用默认值定义了颜色数据,这使我们可以重载构造函数。这种重载意味着我们不需要创建其他构造函数代码来使用同一个 C_Mouse 类。为了区分是在控制指示器中使用还是在 EA 交易中使用,这一点很有必要。如果不存在这样的重载,我们就必须创建另一个构造函数来告诉该类是否在 "完整 "模式下使用。当前在构造函数代码中,我们定义了变量的值。同时,我们也是第一次使用它的值。如果是 "完整 "模式,我们将创建一条在图表中使用的价格线。否则,将不会创建这条线。
析构函数代码也有细微改动,代码如下:
~C_Mouse() { if (CheckPointer(Terminal) == POINTER_INVALID) return; ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, 0, false); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, false); ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair); ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName); }
如果由于指向 C_Terminal 类的指针未正确初始化而导致类构造失败,我们将无法在析构函数中使用该指针。为了避免使用无效指针,我们在类的析构函数中执行了这一检查。这是初始编码中最简单的部分。现在,让我们快速浏览一下旧的类代码,看看需要做哪些修改。
void CreateStudy(void) { if (m_Mem.IsFull) { def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH); def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_WIDTH, 2); CreateObjectInfo(0, 0, def_NameObjectStudy); } m_Info.Study = eStudyCreate; } //+------------------------------------------------------------------+ void ExecuteStudy(const double memPrice) { double v1 = GetInfoMouse().Position.Price - memPrice; int w, h; if (!CheckClick(eClickLeft)) { m_Info.Study = eStudyNull; ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true); if (m_Mem.IsFull) ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName + "T"); }else if (m_Mem.IsFull) { string sz1 = StringFormat(" %." + (string)def_InfoTerminal.nDigits + "f [ %d ] %02.02f%% ", MathAbs(v1), Bars(def_InfoTerminal.szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - 1, MathAbs((v1/ memPrice) * 100.0))); GetDimensionText(sz1, w, h); ObjectSetString(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_TEXT, sz1); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corTrendN : m_Info.corTrendP)); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XSIZE, w); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YSIZE, h); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X - w); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - (v1 < 0 ? 1 : h)); ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, 1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price); ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP)); } m_Info.Data.ButtonStatus = eKeyNull; }
我们对这两个方法进行了修改,使其变得非常简单直接。它们为创建和处理对象添加了一个小检查。只有在 "完整 "模式下使用该类时,才会进行此类操作。但我们在 C_Mouse 类中还有另一个方法。这一点非常重要,因为通过这个方法,我们将与类进行交互。其代码如下:
virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { int w = 0; static double memPrice = 0; switch (id) { case (CHARTEVENT_CUSTOM + ev_HideMouse): if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE); break; case (CHARTEVENT_CUSTOM + ev_ShowMouse): if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH); break; case CHARTEVENT_MOUSE_MOVE: ChartXYToTimePrice(def_InfoTerminal.ID, m_Info.Data.Position.X = (int)lparam, m_Info.Data.Position.Y = (int)dparam, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price); if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineH, 0, 0, m_Info.Data.Position.Price = def_AcessTerminal.AdjustPrice(m_Info.Data.Position.Price)); m_Info.Data.Position.dt = def_AcessTerminal.AdjustTime(m_Info.Data.Position.dt); ChartTimePriceToXY(def_InfoTerminal.ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, m_Info.Data.Position.X, m_Info.Data.Position.Y); if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineV, 0, m_Info.Data.Position.dt, 0); m_Info.Data.ButtonStatus = (uint) sparam; if (CheckClick(eClickMiddle)) if ((!m_Mem.IsFull) || ((color)ObjectGetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy(); if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate)) { ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, false); if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, 0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price); m_Info.Study = eStudyExecute; } if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice); m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute; break; case CHARTEVENT_OBJECT_DELETE: if ((m_Mem.IsFull) && (sparam == def_NameObjectLineH)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); break; } }
与前几个方法一样,这个方法也经过了修改。事实上,我们所做的唯一一件事就是为图表上的对象是否需要操控添加了检查。您可能认为这些检查是不必要的,因为在某些情况下,对象并不在图表上,即使在图表上,也必须以同样的方式进行操控。这是因为 MetaTrader 5 平台不会创建重复对象。但请想一想:当我们对对象进行这些操控时,并不是只做一次。事实上,EA 和控制指示器使用的是同一个类,但却不知道两者同时在使用这个类,这就迫使 MetaTrader 5 平台对对象处理函数进行两次调用。这其实不是问题,但我们必须记住,我们是在使用该系统进行市场回放或模拟。从这个项目一开始,我就多次重申,我们的主要目标是使模拟或回放尽可能接近真实市场的情况。这不仅仅是创建柱形的问题,而是创建柱形所需的时间问题。无论使用何种处理器或硬件,MetaTrader 5 都会在一次调用中访问两次对象,这会浪费宝贵的时间。如果我们开始因为这些代码的重复调用而损失毫秒甚至纳秒,那么很快我们就会开始损失秒或过多地延迟柱形图的创建。因此,我们应该确保 MetaTrader 5 在图表上显示对象时不需要做额外的工作。
我们已经完成了 C_Mouse 类代码的修改,不需要再担心 EA 代码了。目录结构发生了变化,但这只影响所包含的代码。因此,我认为没有必要纠缠于此。让人稍感欣慰的是,EA 代码的实现没有发生任何变化。但这只是暂时的,因为用于控制重放/模拟的控制指示器代码将发生更深层次的变化,这需要详细而冷静的解释。那么,让我们进入下一个话题。
修改控制指示器
要对控制指示器进行修改,我们首先要处理它所使用的类。我指的是 C_Control 类。让我们来看看第一处代码修改:
#include "..\Auxiliar\C_Terminal.mqh" #include "..\Auxiliar\C_Mouse.mqh" //+------------------------------------------------------------------+ #define def_AcessTerminal (*Terminal) #define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() //+------------------------------------------------------------------+ class C_Controls : protected C_Mouse { protected: enum EventCustom {ev_WaitOn, ev_WaitOff}; private : //+------------------------------------------------------------------+ string m_szBtnPlay; bool m_bWait; struct st_00 { string szBtnLeft, szBtnRight, szBtnPin, szBarSlider, szBarSliderBlock; int posPinSlider, posY, Minimal; }m_Slider; C_Terminal *Terminal;
你可以立即看到,代码现在有两个包含文件的调用,包括 C_Terminal 类和 C_Mouse 类。但为什么不使用继承自 C_Mouse 类的 C_Study 类呢?原因是,我们不会让控制指示器去做或管理分析。这是 EA 交易的工作,而不是控制指示器的工作,至少在现阶段是这样。因此,我们将使用 C_Mouse 类。请注意,C_Control 类继承自 C_Mouse 类。尽管有这种继承性,但控制指示器不会从中受益,至少不会直接受益。因此,这种继承甚至可以是私有的,但我通常将这种继承视为受保护的继承。最后需要注意的是 C_Terminal 类将使用的指针。
请注意:在 C_Control 类的源代码中,有一个私有全局变量用于访问图表窗口索引。在此版本中,该索引将不再存在。原因是我们要使用 C_Terminal 类来完成这项工作,而之前是通过 C_Control 类的内部索引来完成的。
由于这一细节,引用该索引的所有地点均已删除。在真正需要的地方,我们用一个允许我们通过 C_Terminal 类访问索引的定义取代了引用。现在让我们看看类的构造函数代码。如下图所示:
C_Controls(C_Terminal *arg) :C_Mouse(arg), m_bWait(false) { if (CheckPointer(Terminal = arg) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); m_szBtnPlay = NULL; m_Slider.szBarSlider = NULL; m_Slider.szBtnPin = NULL; m_Slider.szBtnLeft = NULL; m_Slider.szBtnRight = NULL; }
现在,构造函数将接受一个参数,即指向 C_Terminal 类的指针。它被传递给 C_Mouse 类。但请注意,现在 C_Mouse 不会在完整模式下使用.这样就不会创建 C_Mouse 类中的对象。这是因为它只能作为代码重用的支持。总之,我们要在这里检查这个指针是否有效。这一点很重要,可以避免使用指向无效或未知内存位置的内容。我们还对类的析构函数进行了一些修改:
~C_Controls() { if (CheckPointer(Terminal) == POINTER_INVALID) return; ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, false); ObjectsDeleteAll(def_InfoTerminal.ID, def_PrefixObjectName); }
这种检查可防止尝试使用无效指针,因为构造函数可能已经失败,但析构函数并不知道。通过这种检查,我们可以确保析构函数也知道构造函数失败的原因。因为调用析构函数可能只是为了结束对 C_Controls 类的使用。由于我们现在正在使用系统,一些元素的初始化是在该类之外进行的,因此我们可以对源代码进行额外的修改。
void Init(const bool state) { if (m_szBtnPlay != NULL) return; ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, true); CreateBtnPlayPause(state); GlobalVariableTemp(def_GlobalVariableReplay); if (!state) CreteCtrlSlider(); ChartRedraw(); }
删除上面的几行是因为它们的功能已经在其他地方实现了。这是因为 C_Mouse 类会触发鼠标移动,而 C_Terminal 类会告诉 MetaTrader 5 平台,我们希望在图表中删除对象时收到通知。因此,我们删除了这些行,如果这些行留在代码中,我们可能会在某些情况下看到奇怪的行为。请记住,我们绝不能编写重复代码。这被认为是一个错误,因为它会导致代码在某些情况下难以正确执行,而且很难长期维护。
我们需要修改的下一段代码是类消息处理程序。如下图所示:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { u_Interprocess Info; static int six = -1, sps; int x, y, px1, px2; C_Mouse::DispatchMessage(id, lparam, dparam, sparam); switch (id) { case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOn): if (lparam == 0) break; m_bWait = true; CreateBtnPlayPause(true); break; case (CHARTEVENT_CUSTOM + C_Controls::ev_WaitOff): if (lparam == 0) break; m_bWait = false; Info.u_Value.df_Value = dparam; CreateBtnPlayPause(Info.s_Infos.isPlay); break; case CHARTEVENT_OBJECT_DELETE: if (StringSubstr(sparam, 0, StringLen(def_PrefixObjectName)) == def_PrefixObjectName) { if (StringSubstr(sparam, 0, StringLen(def_NameObjectsSlider)) == def_NameObjectsSlider) { RemoveCtrlSlider(); CreteCtrlSlider(); }else { Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); CreateBtnPlayPause(Info.s_Infos.isPlay); } ChartRedraw(); } break; case CHARTEVENT_OBJECT_CLICK: if (m_bWait) break; if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay = (bool) ObjectGetInteger(def_InfoTerminal.ID, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin = NULL; } Info.s_Infos.iPosShift = (ushort) m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - 1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + 1); break; case CHARTEVENT_MOUSE_MOVE: if (GetInfoMouse().ExecStudy) return; if ((CheckClick(C_Mouse::eClickLeft)) && (m_Slider.szBtnPin != NULL)) { x = GetInfoMouse().Position.X; y = GetInfoMouse().Position.Y; px1 = m_Slider.posPinSlider + def_PosXObjects + 86; px2 = m_Slider.posPinSlider + def_PosXObjects + 114; if ((y >= (m_Slider.posY - 14)) && (y <= (m_Slider.posY + 14)) && (x >= px1) && (x <= px2) && (six == -1)) { six = x; sps = m_Slider.posPinSlider; ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, false); } if (six > 0) PositionPinSlider(sps + x - six); }else if (six > 0) { six = -1; ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true); } break; } }
与以前相比,该代码发生了一些变化。您看到的是最终版本。这样就更容易解释这里发生的事情了。对于控制指示器,您需要了解鼠标的行为。仅使用 CHARTEVENT_MOUSE_MOVE 事件是不够的。我们需要让 C_Mouse 类完成它的工作。请记住,尽管 EA 和指示器一样,都使用 C_Mouse 类,但它们并不交换有关鼠标操作的信息。
我想过实现这种交换,事实上这是可能的。但我们在这样做时遇到了一些问题。
- 如果我们使用 MetaTrader 5 平台的任何功能在 EA 和控制指标之间交换鼠标数据,那么在重放/模拟系统之外使用 EA 时可能会出现问题。我们可能也很难让控制指示器理解鼠标在做什么。
- 另一种方法是通过 DLL 使用共享内存,但这会对平台或重放/模拟服务产生依赖。坦率地说,我现在对建立这种依赖关系不感兴趣。因此,使用共享内存使 MetaTrader 5 不必像管理 C_Mouse 类那样管理自己的内存已不再重要。
这些理由似乎并不十分令人信服,但我想充分利用 MQL5 语言和 MetaTrader 5 平台来构建重播/模拟系统。这将证明,我们能做的事情远远超出许多人对平台或语言的想象。因此,我们需要 C_Mouse 类也能更新与鼠标相关的数据。在这里进行调用即可。由于我们将通过 CHARTEVENT_MOUSE_MOVE 来处理鼠标事件,因此我们可以继续处理这一特定事件。
我们要做的第一件事就是检查用户是否正在图表上进行分析。在这种情况下,C_Control 类中所有与鼠标事件处理相关的内容都应被忽略。从现在起,我们不会在本地处理鼠标事件。相反,我们会询问 C_Mouse 类发生了什么,然后采取适当的决定和行动来完成用户的期望。然后,我们会检查用户是否按下了鼠标左键,以及滑块对象是否出现在图表上(暂停模式)。如果为 "true",我们将检查发生点击的位置。如果它在滑块对象上,只要按下左按钮,它就会做出相应的反应。这样,用户就可以像以前一样将滑块对象从一侧拖到另一侧。
因此,我们要做的就是为 C_Control 类添加一种方法,让它知道用户是否正在进行分析。类似的元素可以用不同的方式来处理,但正如文章开头所说,这给我们带来了一些便利,尽管这些便利现在还不是那么明显。
结论
在这篇文章中,我们介绍了如何在另一个程序(这里是指标)中使用最初为 EA 交易系统开发的类或方法。所有这些工作都将使我们在使用重放/模拟系统时获得更好的体验。因此,用户在开发一种技术或希望测试一种特定的工作方式时,可以使用本系列开发的系统。还有一个问题目前正在解决之中,我们将在下一篇文章中讨论这个问题。因为为了纠正这种情况,我们必须改变一些东西,增加另一些东西。而且,与本文介绍的所有材料一样,对于刚刚开始学习编程的人来说,这已经非常困难了。我不会无缘无故增加难度。
在附件中,您可以找到完整的系统代码。此外,文章末尾还有三组文件,这样您就可以在不同场景下测试系统。通过经常性的测试,您就可以更多地了解它是如何开发的。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/11492


