
从头开始开发智能交易系统(第 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.



我的启动有问题。
有 100 多条错误信息。
Estou com problemas para inicializar.
错误信息多达 100 多条。
O problema esta na LINHA 17 deste arquivo de cabeçalho C_ChartFloating.mqh ...删除第 17 行的内容 ...因此,该代码将被自动编译 ...
感谢您报告问题 ...😁👍