
从头开始开发智能交易系统(第 8 部分):概念上的飞跃
概述
有时,在开发一些项目时,我们也许会发现新的思路和新的可能特性,这些特性能够派上用场,并为我们正在创建的系统提供极大的改进。 但问题出来了:实现新功能的最简单途径是什么?
问题在于,有时我们不得不忘记已经开发的一切,并从头重新开始。 这令人十分沮丧。 随着时间的推移,在经历了 20 多年的 C++ 编程之后,我提炼出了某种思路。 我们开发出一些概念,帮助我们计划事物,并可以最小的代价进行修改;但有时事物会发生变化,变得比我们最初估计的要复杂得多。
直至目前,我们一直在以这种途径构建 EA,即它可以接收新代码,而不必丢失当前功能:我们只是简单地创建和添加新类。 而现在,我们需要后撤一步,然后向前迈出两步。 后撤一步将允许我们引入新的功能。 该功能是一个窗口类,包含一些基于模板的信息;这是本文的第一部分。 我们将彻底修改代码,同时保留目前所有的功能;而在第二部分中,我们将处理 IDE。
计划
我们的智能交易系统目前是在对象类中构造的。 正如下图所见。
该系统目前运行良好,非常稳定。 但现在,我们必须如下所示重构 EA。 您也许已注意到还有一个额外的类,而 C_TemplateChart 和 C_SubWindow 的位置业已更改。
此种重构的目的是什么? 问题是,以浮动窗口的实现方式不适合包含资产数据的窗口,因此有必要修改该类。 然而,这种变化不仅在结构项上更具美感,当然还需要对代码进行极端的修改,故此它将与以前的代码有很大的区别。
那么,我们就开始工作吧。
实际的实现
1. 智能交易系统内部代码的变更
第一个重大变化是从 EA 初始化文件开始。 参见以下代码:
input group "Window Indicators" input string user01 = ""; //Subwindow indicators input group "WallPaper" input string user10 = "Wallpaper_01"; //Used BitMap input char user11 = 60; //Transparency (from 0 to 100) input C_WallPaper::eTypeImage user12 = C_WallPaper::IMAGEM; //Chart background type input group "Chart Trader" input int user20 = 1; //Leverage factor input int user21 = 100; //Take Profit (financial) input int user22 = 75; //Stop Loss (financial) input color user23 = clrBlue; //Price line color input color user24 = clrForestGreen; //Take Profit line color input color user25 = clrFireBrick; //Stop line color input bool user26 = true; //Day Trade? input group "Volume At Price" input color user30 = clrBlack; //Bar color input char user31 = 20; //Transparency (from 0 to 100 ) //+------------------------------------------------------------------+ C_TemplateChart Chart; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { static string memSzUser01 = ""; Terminal.Init(); WallPaper.Init(user10, user12, user11); if (memSzUser01 != user01) { Chart.ClearTemplateChart(); Chart.AddThese(memSzUser01 = user01); } Chart.InitilizeChartTrade(user20, user21, user22, user23, user24, user25, user26); VolumeAtPrice.Init(user24, user25, user30, user31); OnTrade(); EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+
注意,现在只有一个变量来指示将加载哪些模板。 除了高亮显示的部分外,代码的其余部分似乎保持原样。 也许还未彻底清楚它在这里做了什么,也不清楚为什么这段高亮显示的代码被放在 EA 的初始化部分。 当我们把 EA 加载到图表上时,它会创建一些东西,在正常操作中还可以修改它们。 以前,所添加的高亮显示代码是没有意义的,因为一切都是为了协同工作,修改不会影响 EA 行为或外观。 但是,当我们添加浮动指标时,会发生一些恼人的事情:每次我们变更时间帧时,EA 就会重新启动,窗口也会回归其原始状态。 如果您不去销毁它们,那么废品会堆积在图表上;如果您销毁它们不正确,则它们会在原来的地方重建,这也是一个极大的麻烦。 如果用户没有更改所需的模板,高亮显示的代码会防止浮动窗口被错误地销毁,但如果模板发生了变更,则 EA 将重新正常启动。 这非常简单,但极具效率。
接下来要注意的是有关内部消息传递系统。 以前它有一个附加变量,不过我们把它删除了,因此代码现在如下所示:
void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, NanoEA.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); }
系统现在能更有效地运用 MQL5 消息交换系统,而消息发送与 OnChartEvent 函数本身非常相似。 这允许在没有任何压力的情况下将参数传递给对象类,从而每个类都能以最合适的方式处理 MetaTrader 5 系统生成的事件消息。 通过这种途径,我们进一步把每个对象类隔离开,因此 EA 可以针对每种类型的用户采用更加多样化的形式,且所需代价最少。
2. 子窗口变更的支持代码
直到此刻,子窗口代码都非常简单,但它有一个问题:出于这样或那样的原因,EA 无法删除已创建子窗口。 重新打开智能交易系统时,它会创建一个新的子窗口,导致系统失去对其的控制。 令人惊讶的是,修复这个问题极其容易。 首先,查看下面显示的支持文件片段:
int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport"); return INIT_SUCCEEDED; }
高亮显示的行将为支持文件创建别名。 EA 能看到此 EA 的别名 ,并检查子窗口系统是否已加载。 由于 EA 只检查别名,因此该操作的执行与文件名无关。 稍后将使用相同的代码类型来支持 EA 中的其它内容。 在此我不会详细介绍,但稍后,我会在另一篇文章中,解释如何运用高亮显示的代码。
现在,我们看看加载和创建子窗口的代码。 如下所示:
void Init(void) { int i0; if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -1) ChartIndicatorAdd(Terminal.Get_ID(), i0 = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, 0, "::" + def_Resource)); m_IdSubWinEA = i0; }
如您所见,这有多简单,但此代码不是公开的,而是由另一个公开代码访问:
inline int GetIdSubWinEA(void) { if (m_IdSubWinEA < 0) Init(); return m_IdSubWinEA; }
但为什么要以这种方式实现呢? 也许会发生这样的情况,即 EA 在子窗口中不用任何指标,当系统意识到这一点时,它会从图表中删除子窗口,并仅在必要时才创建它。 但这个决定不是由 EA 代码做出的,而是出自 C_TemplateChart 类。
查看以下动画:
注意,我们现在有一条垂直线,提示我们正在分析的位置。 这些线相互独立。 以前没有提供这些数据,因此很难根据图表来分析指标的某些点。 这是 C_TemplateChart 类中包含的改进之一。 我们来看看类中的代码,以便了解进一步的变化,因为它们是实质性的。
我们看看以下声明变量的代码:
class C_TemplateChart : public C_SubWindow { #define def_MaxTemplates 8 #define def_NameTemplateRAD "IDE" //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eParameter {TEMPLATE = 0, PERIOD, SCALE, WIDTH, HEIGHT}; //+------------------------------------------------------------------+ struct st { string szObjName, szSymbol, szTemplate, szVLine; int width, scale; ENUM_TIMEFRAMES timeframe; long handle; }m_Info[def_MaxTemplates]; int m_Counter, m_CPre, m_Aggregate; struct st00 { int counter; string Param[HEIGHT + 1]; }m_Params;
首先要注意的是,C_TemplateChart 类将扩展 C_SubWindow 类。 这部分代码似乎并无特别之处,但请注意高亮显示的部分:它指向一个内部数据分析系统,通过该系统可以相应地创建和呈现用户请求的指标。 现在,针对描述用户将如何指示事物的系统开始标准化。 即使它看起来令人困惑,但随着时间的推移,它会变得越来越清晰。 为了解释新格式,有必要分析以下片段,该片段负责分析用户的请求:
int GetCommand(int iArg, const string szArg) { for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = ""; m_Params.counter = 0; for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg]) { case ')': case ';': m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++); return iArg + 1; case ' ': c2 += (c1 == iArg ? 0 : 1); c1 = (c1 == iArg ? iArg + 1 : c1); break; case '(': case ',': c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2); m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); c2 = 0; c1 = iArg + 1; break; default: c2++; break; } return -1; }
首先,清理结构中以前的所有数据。 然后开始逐个分析并接收参数(如果有)。 它们不是必需的,但它们指明了如何向用户呈现内容。 该系统是自扩展的。 故此,如果您打算添加更多信息,您只需在 eParameter 枚举中指定即可。 目前,系统有五个参数,它们的指定顺序应与 eParameters 枚举中所示的顺序相同。 该枚举在变量声明部分高亮显示。 参数的正确顺序及其解释如下所示。
参数 | 结果 |
---|---|
1. TEMPLATE 或 ASSET | 指定是要查看模板亦或资源 |
2. PERIOD | 如果已指定,将按照指定的固定时间段设置指标,就像以前一样 |
3. SCALE | 如果已指定,指标将绑定到固定标尺。 |
4. WIDTH | 如果已指定,按照参数设置指标在窗口中的宽度。 |
5. HEIGHT | 这个新参数将在本文后面讨论 — 它表示使用浮动窗口。 |
目前唯一不能从参数中获益的结构是 IDE,但这会得到修复,在下一篇文章中,我将展示如何从 IDE 中获益。 本文的重点是关注其它系统。
现在假设出于某种原因,我们希望允许用户控制指标垂直线的颜色。 参数分析代码无需进行任何更改 — 简单地按以下方式进行修改:
class C_TemplateChart : public C_SubWindow { #define def_MaxTemplates 8 #define def_NameTemplateRAD "IDE" //+------------------------------------------------------------------+ private : //+------------------------------------------------------------------+ enum eParameter {TEMPLATE = 0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT}; //+------------------------------------------------------------------+ // ... Internal code .... struct st00 { int counter; string Param[HEIGHT + 1]; }m_Params;
系统将自动识别在一次调用中的六个参数。 现在又出现了另一个问题。 虽然上面的 GetCommand 代码工作正常,但其中存在一个漏洞。 当我们创建自用系统时,我们往往不会注意到这个漏洞。 但若把该系统提供给其他人使用时,错误就会变得很明显,对于如何解决该问题,也许会令一些经验不足的程序员束手无策。 这就是为什么面向对象编程受到高度重视的原因 — 它允许为最感兴趣的程序创建最合用的模型。 OOP 的先决条件之一是确保数据和类变量的正确初始化。 为了保证这一点,必须测试所有内容。 虽然 GetCommand 代码看起来正确,但实际上它包含一个漏洞 — 它没有检查最大参数限制。 如果原始模型只接受五个参数,那么如果用户设置了六个参数会发生什么? 这是需要避免的:我们不应该假设一切都会成功,而应该保证一切都会成功。 因此,需要按如下所示调整代码(调整高亮显示部分)。
int GetCommand(int iArg, const string szArg) { for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = ""; m_Params.counter = 0; for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg]) { case ')': case ';': m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++); return iArg + 1; case ' ': c2 += (c1 == iArg ? 0 : 1); c1 = (c1 == iArg ? iArg + 1 : c1); break; case '(': case ',': if (m_Params.counter == HEIGHT) return StringLen(szArg) + 1; c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2); m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2); c2 = 0; c1 = iArg + 1; break; default: c2++; break; } return -1; }
通过简单地添加一行,我们就能防止系统发生意外结果,因为如果期望的最后一个参数是 HEIGHT,但实际上却不是最后一个,这在逻辑上意味着有问题,因此系统会忽略随后声明的所有内容,来避免出现更多问题。
如果您不了解系统是如何识别模式和参数,请使用以下语法:
Parameter_00 (Parameter_01, Parameter_02, Parameter_03, Parameter_04)
其中,Parameter_00 指定要所用的模板,其余的则用逗号(,)分隔,且指示的数值均应在 eParameter 枚举中定义。 如果我们只想更 Parameter_03,,那么可以将其余的保留为空,如下图所示。 在该示意图中,我展示的系统会按照用户想要的方式工作。
请注意,我们有一个标准化的指示,它与函数调用非常相似,但这似乎令人困惑。 下面是实际发生的情况:我们指定 RSI 模板,然后不指定周期或标尺。 这些值保留为空,如此系统即可理解,它必须遵循主图表。 但我们指定了宽度和高度,因此系统理解这应该显示在浮动窗口中,故 RSI 指标显示在浮动窗口中。 在 ADX 模板中,我们仅指示宽度,如此系统就按子窗口中定义的宽度来显示它。 Stoch 指标将占用剩余的整个子窗口,与 ADX 共享空间。 但如果用户想要更改某些内容,这并不困难,看看当我们指定 ADX 的高度时会发生什么。
系统将立即改变 ADX 的显示方式 — 将其置于浮动窗口当中,同时将整个子窗口留给 Stoch。 每个浮动窗口彼此完全独立。 但它们未超出我们所能看到的范围,请看下一张图片。
请注意,子窗口已被删除,因为不再需要它。 但是,哪个函数负责所有这些? 如下所示 — 只需稍作修改即可完成许多有趣的事情:
void AddTemplate(void) { ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; string sz0 = m_Params.Param[PERIOD]; int w, h, i; bool bIsSymbol; if (sz0 == "1M") timeframe = PERIOD_M1; else if (sz0 == "2M") timeframe = PERIOD_M2; else if (sz0 == "3M") timeframe = PERIOD_M3; else if (sz0 == "4M") timeframe = PERIOD_M4; else if (sz0 == "5M") timeframe = PERIOD_M5; else if (sz0 == "6M") timeframe = PERIOD_M6; else if (sz0 == "10M") timeframe = PERIOD_M10; else if (sz0 == "12M") timeframe = PERIOD_M12; else if (sz0 == "15M") timeframe = PERIOD_M15; else if (sz0 == "20M") timeframe = PERIOD_M20; else if (sz0 == "30M") timeframe = PERIOD_M30; else if (sz0 == "1H") timeframe = PERIOD_H1; else if (sz0 == "2H") timeframe = PERIOD_H2; else if (sz0 == "3H") timeframe = PERIOD_H3; else if (sz0 == "4H") timeframe = PERIOD_H4; else if (sz0 == "6H") timeframe = PERIOD_H6; else if (sz0 == "8H") timeframe = PERIOD_H8; else if (sz0 == "12H") timeframe = PERIOD_H12; else if (sz0 == "1D") timeframe = PERIOD_D1; else if (sz0 == "1S") timeframe = PERIOD_W1; else if (sz0 == "1MES") timeframe = PERIOD_MN1; if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) return; bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true); w = (m_Params.Param[WIDTH] != "" ? (int)StringToInteger(m_Params.Param[WIDTH]) : 0); h = (m_Params.Param[HEIGHT] != "" ? (int)StringToInteger(m_Params.Param[HEIGHT]) : 0); i = (m_Params.Param[SCALE] != "" ? (int)StringToInteger(m_Params.Param[SCALE]) : -1); i = (i > 5 || i < 0 ? -1 : i); if ((w > 0) && (h > 0)) AddIndicator(m_Params.Param[TEMPLATE], 0, -1, w, h, timeframe, i); else { SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w); if (!ChartApplyTemplate(m_Info[m_Counter - 1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - 1].handle, "Default.tpl"); if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD) { C_Chart_IDE::Create(GetIdSubWinEA()); m_Info[m_Counter - 1].szVLine = ""; }else { m_Info[m_Counter - 1].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand(); ObjectCreate(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJ_VLINE, 0, 0, 0); ObjectSetInteger(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJPROP_COLOR, clrBlack); } ChartRedraw(m_Info[m_Counter - 1].handle); } }
高亮显示的部分示意数据在屏幕上的显示方式选择。 该代码与之前的已有代码没有太大区别。 但这些测试保证了系统将按照用户想要的方式运行。 到目前为止,所有这些都不需要在新模型中重新构造代码,但当我们查看负责调整子窗口大小的函数时,情况会发生变化。
void Resize(void) { #define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B) int x0, x1, y; if (!ExistSubWin()) return; x0 = 0; y = (int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA())); x1 = (int)((Terminal.GetWidth() - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1)); for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++) { macro_SetInteger(OBJPROP_XDISTANCE, x0); macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1)); macro_SetInteger(OBJPROP_YSIZE, y); if (m_Info[c0].szTemplate == "IDE") C_Chart_IDE::Resize(x0); } ChartRedraw(); #undef macro_SetInteger }
高亮显示的行可防止系统在老旧的基础上构建。 此行将检查是否有 EA 打开和维护的子窗口,如果没有,则返回,函数不执行其它操作。 但如果存在这样一个子窗口,则应根据需要调整其中所有内容的大小,并且正是因为进行了此测试,系统才得以彻底改造。
下面是另一个经过修改的函数:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { int mx, my; datetime dt; double p; C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam); switch (id) { case CHARTEVENT_MOUSE_MOVE: mx = (int)lparam; my = (int)dparam; ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (int c0 = 0; c0 < m_Counter; c0++) if (m_Info[c0].szVLine != "") { ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, 0, dt, 0); ChartRedraw(m_Info[c0].handle); } break; case CHARTEVENT_CHART_CHANGE: Resize(); for (int c0 = 0; c0 < m_Counter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe); ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_CHART_SCALE,(m_Info[c0].scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Info[c0].scale)); } break; } }
高亮显示的部分确实值得特别注意。 它的作用是什么? 此代码将在正确的位置和正确的模板中显示垂直线。 代码的其余部分将在图表变更时简单地维护和调整模板。
在对象类中进行此操作,比之在 EA 的 OnChartEvent 系统中有更多好处。 主要是,每个类都可以处理 MetaTrader 5 发送给 EA 的事件。 我们可令每个类完成它的工作,替代将所有内容集中在一个函数中;如此,如果我们不想在 EA 中使用该类,我们就可简单地删除它,而不会对其余代码产生任何副作用。
这样的编程很优美,是不是呢? 我热爱编程...
在继续讨论本文的下个论点之前,我们将简要评论参数 1 和 2 中可以使用的值。 参数 1 可赋予以下数值: 1M, 2M, 3M, 4M, 5M, 6M, 10M, 12M, 15M, 20M, 30M, 1H, 2H, 3H, 4H, 6H, 8H, 12H, 1D, 1S, 1MONTH。 这些值不是随机的 — 它们来自 ENUM_TIMEFRAME 枚举,并且完全重复您使用常规图表时的操作。 参数 2 可以取 0 到 5 之间的值,其中 0 是最远的标尺,5 是最靠近的标尺。 有关更多详细信息,请参阅 CHART_SCALE。
3.4 支持浮动窗口
现在我们来了解浮动窗口是如何创建和维护的,因为如果不了解这一点,就不可能真正运用系统。 负责此操作的对象类名为 C_ChartFloating。 有人可能会想:为什么不用 MQL5 标准库中的 Control 类? 原因很简单。 控件类允许我们创建和维护一个窗口,该窗口的功能与机器上的操作系统非常相似,但对于我们的目的而言,它过于夸张了。 我们需要更简约的东西。 使用控件类做我们想做的事情就像使用火箭筒杀死苍蝇一样,因此我们使用 C_ChartFloating 类 — 它包含支持浮动窗口所需的最少元素,同时允许我们控制它们。
这个类本身不需要太多解释,因为我们只需要创建 4 个图形对象。 但在内部功能中,有两个地方值得特别注意。 我们从创建窗口的函数开始:
bool AddIndicator(string sz0, int x = 0, int y = -1, int w = 300, int h = 200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, int Scale = -1) { m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); if (m_MaxCounter >= def_MaxFloating) return false; y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y); CreateBarTitle(); CreateCaption(sz0); CreateBtnMaxMin(); CreateRegion(TimeFrame, Scale); m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID); ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl"); m_Win[m_MaxCounter].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand(); ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, 0, 0, 0); ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack); m_Win[m_MaxCounter].PosX = -1; m_Win[m_MaxCounter].PosY = -1; m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x; m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y; SetDimension(w, h, true, m_MaxCounter); SetPosition(x, y, m_MaxCounter); ChartRedraw(m_Win[m_MaxCounter].handle); m_MaxCounter++; return true; }
该代码创建了所有必要的支持,如此您就可以在 CHART 对象上应用模板。 它是在高亮显示的代码部分实现的,请注意,调用此函数所需的真正唯一参数是模板名称。 所有其它值都已预先初始化,但没有任何东西阻止您指定需要哪些值。 对于创建的每个新窗口,下一个窗口将略微偏移,如此在默认情况下就不会与其它窗口重叠。 如下行所示:
y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);
该类中下一个有趣的函数可处理消息。 此为其代码:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { int mx, my; datetime dt; double p; static int six = -1, siy = -1, sic = -1; switch (id) { case CHARTEVENT_MOUSE_MOVE: mx = (int)lparam; my = (int)dparam; if ((((int)sparam) & 1) == 1) { if (sic == -1) for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--) sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -1); if (sic >= 0) { if (six < 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false); six = (six < 0 ? mx - m_Win[sic].PosX : six); siy = (siy < 0 ? my - m_Win[sic].PosY : siy); SetPosition(mx - six, my - siy, sic); } }else { if (six > 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true); six = siy = sic = -1; } ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p); for (int c0 = 0; c0 < m_MaxCounter; c0++) ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, 0, dt, 0); break; case CHARTEVENT_OBJECT_CLICK: for (int c0 = 0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin) { SwapMaxMin((bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0); break; } break; case CHARTEVENT_CHART_CHANGE: for(int c0 = 0; c0 < m_MaxCounter; c0++) { ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame); ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale)); } m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS); m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS); break; } for (int c0 = 0; c0 < m_MaxCounter; c0++) ChartRedraw(m_Win[c0].handle); }
此函数实现了 C_ChartFloating 类支持的所有事件处理。 无论有多少个窗口,它都将以相同的方式处理。 如果我们在智能交易系统的 OnChartEvent 函数中执行此操作,该函数将非常复杂,并且不太稳定。 通过在对象类中实现这里的功能,我们就能保证代码的完整性。 因此,如果我们不需要用到浮动窗口,那么我们所需要做的就是从类和可以访问文件的地方删除文件。 这样的实现令您的代码更快捷、更容易。
上述代码中还有一个有趣的部分。 它已被高亮显示,其内部代码如下:
void SwapMaxMin(const bool IsMax, const int c0) { m_Win[c0].IsMaximized = IsMax; SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : 100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : 0), false, c0); SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0); }
以上代码起什么作用? 看起来太困惑了? 为了理解,我们仔细看看下面的动画。
当创建浮动窗口时,它有一个初始锚点,可由程序员或类本身的定位系统指定。 该定位点对于最大化窗口和最小化窗口都是相同的。 这些值不是固定的,即用户可以轻松更改这些点。
假设您需要空白图表上的某个位置,那么您可以将最大化窗口移到易于快速阅读的位置,然后最小化同一窗口,并将其移到另一个位置,例如屏幕的一角。 系统记住它,当您最大化窗口时,它将跳到最小化之前的最后那个锚点。 当窗口最小化时,相反的情况同样适用。
结束语
目前全部内容就是这些了。 在下一篇文章中,我们将把此功能扩展到 IDE 支持类。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10353


