
在一张图表上的多个指标(第 03 部分):为用户开发定义
概述
在“一张图表上的多个指标”的前一篇文章中,我们研究了允许在图表子窗口中加载多个指标的基本代码。 但其所代表的只是一个更大系统的起点。 基于这个模型还可以做一些不同的事情。 但我们应该循序渐进,因为这些文章的目标之一是鼓励您学习如何编程,从而您可以根据自己的想法自行设计系统。 在本文中,我们将扩展该功能。 对于那些已经喜欢该系统之所能,但希望其能做得更多的人来说,这也许很有趣。
计划
通常,当我们开始实现一个新系统时,我们并不真正知道我们如何对其进行改进;故此,我们应该始终启动一个新项目,并着眼于未来进行改进。 这对那些刚开始的人来说非常重要:持续地规划一些事情,想象未来的扩展和改进。
核心代码根本没有改变,这在某种意义上是意料之中的。 但是对象类的代码已经发生了巨大的变化。 随着代码重用性变得越加重要(这是面向对象编程的基本思想之一:始终重用,仅在必要时创建新代码),我们进行了这些修改,以实现新功能,并能够以更灵活的方式创建新的改进。 如此,我们来看看新的对象类。 我会高亮示意这些变化,令其更容易理解。
我们从类的私密变量新定义开始。
struct st { string szObjName, szSymbol; int width; }m_Info[def_MaxTemplates]; int m_IdSubWin, m_Counter, m_CPre, m_Aggregate; long m_Id, m_handle; ENUM_TIMEFRAMES m_Period;
请注意,所变量数量显著增加。 这是因为我们需要更多数据来正确管理新功能。 现在我们的变量系统有了一个结构。 这种结构非常适合将相关变量组合在一起 — 它们确保我们在处理数据时能够快速、轻松地访问。
void SetBase(const string szSymbol, int iScale, int iSize) { #define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[m_Counter].szObjName, A, B) if (m_IdSubWin < 0) { m_Id = ChartID(); m_IdSubWin = (int)ChartGetInteger(m_Id, CHART_WINDOWS_TOTAL) - 1; m_Aggregate = 0; } m_Info[m_Counter].szObjName = __FILE__ + (string) MathRand() + (string) ObjectsTotal(m_Id, -1, OBJ_CHART); ObjectCreate(m_Id, m_Info[m_Counter].szObjName, OBJ_CHART, m_IdSubWin, 0, 0); ObjectSetString(m_Id, m_Info[m_Counter].szObjName, OBJPROP_SYMBOL, (m_Info[m_Counter].szSymbol = szSymbol)); // .... macro_SetInteger(OBJPROP_PERIOD, m_Period); m_handle = ObjectGetInteger(m_Id, m_Info[m_Counter].szObjName, OBJPROP_CHART_ID); m_Aggregate += iSize; m_Info[m_Counter].width = iSize; m_CPre += (iSize > 0 ? 1 : 0); m_Counter++; #undef macro_SetInteger };
我们很快将看到的主要变化是,我们正在使用一个结构来存储资产名称、对象名称及其宽度。 现在我们还可以指定指标在子窗口中的宽度。 我们来做些注释,方便在类的其它部分使用它们。 下面是变化最大的函数。
void Decode(string &szArg, int &iScale, int &iSize) { #define def_ScaleDefault 4 #define macro_GetData(A) \ b0 = false; \ for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++); \ for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++); \ if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = ""; string sz1; int i0, i1, c1 = StringLen(szArg); bool b0 = true; StringToUpper(szArg); iScale = def_ScaleDefault; m_Period = _Period; for (int c0 = 0, max = StringLen(szArg); c0 < max; c0++) switch (szArg[c0]) { case ':': b0 = false; for (; (c0 < max) && ((szArg[c0] < '0') || (szArg[c0] > '9')); c0++); iScale = (int)(szArg[c0] - '0'); iScale = ((iScale > 5) || (iScale < 0) ? def_ScaleDefault : iScale); break; case ' ': break; case '<': macro_GetData('>'); if (sz1 == "1M") m_Period = PERIOD_M1; else //.... if (sz1 == "1MES") m_Period = PERIOD_MN1; break; case '[': macro_GetData(']'); iSize = (int) StringToInteger(sz1); break; default: c1 = (b0 ? c0 : c1); break; } szArg = StringSubstr(szArg, 0, c1 + 1); #undef macro_GetData #undef def_ScaleDefault }
绿色示意代码的新增内容。 黄色示意源代码中已经存在,但由于实际原因被移动的行。 现在我们来看看所有这些加入代码做了什么,更重要的是,它们针对原始系统的功能方面做了哪些改进。 实际上,我们正在打造一个基础,让用户能够定制一些特殊的东西。 我们尝试往向现有语法中添加新规则来实现这一点(请参见下表):
分隔符 | 功能 | 示例 | 结果 |
---|---|---|---|
< > | 指定要使用的图形周期 | < 15m > | 将指标周期固定为 15 分钟。 原始图表可以使用不同的时间帧,但指标仅显示 15 分钟时间帧的数据。 |
[ ] | 指定指标宽度 | [ 350 ] | 指标宽度固定为 350 像素。 |
固定图表周期的分隔符 — 仅在指标窗口中,且不会影响用户所作的任何更改。 所有其它指标和主图表将依据用户选择的新图表周期进行更新,但固定指标不会跟随新图表周期。 在某些情况下,这可能很有趣,如下图所示。
我们需要在交易屏幕上显示同一资产不同时期的图表,而这大大方便了各种类型的设置。 现在,在很特别的情况下,一个固定图表宽度的分隔符会令这些设置变得更容易。 它还有一个很大的用途,我们会在另一篇文章中探讨,但现在您可以用它来控制指标的宽度。
您可以利用所有分隔符的组合,也可只用指标中真正需要的分隔符,因为这里并无具体的规则。 唯一的规则是,指标名称应该放在其它所有内容之前。 我们回到代码解释。 看下面几行:
#define macro_GetData(A) \ b0 = false; \ for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++); \ for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != A); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++); \ if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = ""; //.... case '<': macro_GetData('>');
请注意,我们正在定义一些对许多人来说可能感觉很奇怪的东西。 但其名称在此很有用:macro_GetData(A),因其是个宏定义,故会创建一段代码。 当编译器在代码中发现这个宏定义时,编译器将用宏定义代码替换声明。 如果我们要在多个地方重复某个代码片段,但期望在一个声明和另一个声明之间进行最小的修改,这种方式将非常有用。 在上一个示例中,绿色的行会被替换,编译器生成的代码如下所示:
case '<': b0 = false; for (c0++; (c0 < max) && (szArg[c0] == ' '); c0++); for (i0 = 0, i1 = c0; (c0 < max) && (szArg[c0] != '>'); i0 = (szArg[c0] != ' ' ? c0 - i1 + 1 : i0), c0++); if (szArg[c0] == A) sz1 = StringSubstr(szArg, i1, i0); else sz1 = ""; //....
这种东西是最真实的哲学表达:尽可能多地重复使用,尽可能少地写新东西。 现在,我们看看如果我们想让语法更清晰,可以如何进行修改。 这是一个小细节,但可依据您的个人风格量身定制,如此可能会更好。 请看下面一行:
//..... if (sz1 == "1M") m_Period = PERIOD_M1; else //.....
高亮示意的信息是一个重要的细节。 根据我所用的模型,如果用户想要使用 1 分钟时间帧,那么用户应该使用以下语法来表达:MIN_1。 如果您想拥有一个独立的风格,您可以用自己的方式来指定。 但是字母应该是大写的,不含空格。 例如,所选部分可以替换为 "1MIN","MIN_1","1_MINUTE" 或者更详细的东西,比如: "LOCK_IN_1_MIN" 或以您的语言 - 只要单词之间没有空格,就行。 实际上,这个空格限制是可以取消的,但在我看来,这样做并无实际必要。 看看通晓编程有多好 — 您可以用自己的独特风格来使用。 我修改的下一个代码片段是默认的析构函数。
~C_TemplateChart() { for (char c0 = 0; c0 < m_Counter; c0++) { ObjectDelete(m_Id, m_Info[c0].szObjName); SymbolSelect(m_Info[c0].szSymbol, false); } }
针对以下情况添加了高亮示意的代码行:如果资产并非在单独的窗口中打开,则它不应再出现在市场观察之中。 这样可以防止使用未出现的资产,占用空间并污染窗户。 现在我们看看在上一篇文章中承诺要解释的内容。 这在最初的代码中并不存在,但将来它将成为代码的一部分。
void AddTemplate(const eTypeChart type, const string szTemplate, int scale, int iSize) { if (m_Counter >= def_MaxTemplates) return; if (type == SYMBOL) SymbolSelect(szTemplate, true); SetBase((type == INDICATOR ? _Symbol : szTemplate), scale, iSize); if (!ChartApplyTemplate(m_handle, szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, "Default.tpl"); ChartRedraw(m_handle); }
如果所有观察到的资产套用相同的设置,则高亮示意的行使用默认设置文件。 若事情没有按预期进行,且您并未真正理解应该如何采取实际行动,那么这件事是不会做好的。 但如果设置完全相同,为什么不使用它们呢? 请注意,如果找不到指定资产的设置文件,则可以理解为您希望使用 MetaTrader 5 默认的设置,这些设置在 DEFAULT.TPL 文件中定义,其位于 Profiles\Template 目录当中。 但首先我们需要了解一件重要的事情。 为什么我没有在 ChartApplyTemplate 函数中指定一个目录? 这是因为 MetaTrader 5 遵照一定的逻辑执行搜索。 了解这种逻辑是如何运作的,有助于您以更有趣、压力更小的方式理解状况。
想象一下下面的场景,我们用下面一行替换高亮示意的行:
if (!ChartApplyTemplate(m_handle, "MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""MyTemplates\\Default.tpl");
MetaTrader 5 首先在自定义指标可执行文件所在目录的 MYTEMPLATES 子目录中搜索设置文件,也就是说,如果所创建的多个指标的可执行文件处于同一文件夹中,且含有 MYTEMPLATES 文件夹,MetaTrader 5 则在该文件夹中搜索设置文件。 不过,若该处没有发现,则它将在 MQL5\Profiles\Templates\MyTemplates 目录中搜索相同文件,这就是为什么我以前没有展示这个。 但它不仅如此。 同一代码段中还有另一个细节:
if (!ChartApplyTemplate(m_handle, "\\MyTemplates\\" + szTemplate + ".tpl")) if (type == SYMBOL) ChartApplyTemplate(m_handle, ""\\MyTemplates\\Default.tpl");
一个小细节改变了一切:现在 MetaTrader 5 首先尝试在 MQL5 MyTemplates 目录中查找该文件,如果未找到该文件,它则遵循上述步骤。 您可以在 ChartApplyTemplate 文档中找到这些信息,因为我不想让那些不知道 MetaTrader 5 如何工作的人感到困惑。 但是现在您已经足够了解搜索是如何执行的,故您可以创建变体,因为您知道文件放在何处。
在我们的类里,下一个函数发生了重大变化,如下所示:
void Resize(void) { #define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_Info[c0].szObjName, A, B) int x0 = 0, x1, y = (int)(ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS, m_IdSubWin)); x1 = (int)((ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS, m_IdSubWin) - 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); } ChartRedraw(); #undef macro_SetInteger }
所选行示意此函数中最重要的计算。 它们会按照用户想要的方式调整窗口,但已设置为固定大小的窗口将创建一个自由区域,其余的会有自己定义的尺寸。 但如果主窗口的宽度减小,蓝色的计算代码将不允许减小固定尺寸窗口的宽度,这可能会导致问题。 但我们暂时不谈这个问题,因为固定宽度的窗户还有其它好处,在未来会进行更好的探索。 在我们的类中最后一个变化如下:
void AddThese(const eTypeChart type, string szArg) { string szLoc; int i0, iSize; //.... Decode(szLoc, i0, iSize); AddTemplate(type, szLoc, i0, iSize); //.... }
唯一的变化以高亮示意,所以没有什么特别的。
结束语
我希望这篇文章和其它文章一样,能展示结构化编程是多么有趣。 配合一些调整和耐心,我们可以为系统添加很多功能。 代码重用将令维护更加容易,因为若是复用完善的代码越多,包含错误的可能性就越小。 在下一篇文章中,我们将把这个系统引入对许多人来说更有趣的其它地方,并尝试了解编程的更多细节。 期待再见...
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10239

