
创建一个行情卷播面板:改进版
概述
在上一篇文章创建一个行情卷播面板:基本版中,我们已经看到了如何以指标形式创建实时显示品种价格卷播面板。 然而,在上一篇文章中,我们并未完全实现指标,不是因为它不可能,而是因为我们的目标是展示创建指标的过程,看看如何让它用最少的代码工作,以便给人一种它在移动的印象。
我们已经看到,没有必要创建特殊的代码,或复杂的计算就能令面板从右向左移动,反之亦然。 用户唯一需要做的就是指出数值要增加亦或减少。 只有如此,才能令指标向右、向左移动,或保持静止。
但这些数据并不总是足够的,且不能始终反映人们真正希望在面板上看到的内容。 要是能有更多细节,那就更好了。 这就是我们将在这里实现的内容。 我们还会实现更多内容,从而令面板更实用。 仅凭新形式的面板事实上仍然是完全无用的,因为还有其它更合适的方式来创建它。
故此,我们要做的第一件事是修改视图,能够添加图像,诸如资产徽标或其它图像,如此用户即可快速、轻松地识别所示的资产。 他们说一张图片胜过千言万语。 我们来看看是否真的如此。
实现新的报价面板系统
在这个新版实现中,我们要做的第一件事是创建一个新的对象类,它是抽象对象,可呈现资产图像。 我们用到以下代码:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include "C_Object_Base.mqh" //+------------------------------------------------------------------+ class C_Object_BtnBitMap : public C_Object_Base { public : //+------------------------------------------------------------------+ void Create(string szObjectName, string szResource1) { C_Object_Base::Create(szObjectName, OBJ_BITMAP_LABEL); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 0, "\\Images\\Widget\\Bitmaps\\" + szResource1 + ".bmp"); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, false); }; //+------------------------------------------------------------------+ };
在解释这里发生的事情之前,我简要解释一下我们为什么要这样做。 我们首先创建一个位图对象来存储图像。 我们使用通用类来简化对象创建。 然后,我们根据图像位置和名称通知对象它应该使用哪个图像。 最后,我们通知对象要显示的图像是索引 0 处的图像。 以这种方式,我们在系统中达成了非常高的抽象级别。
现在,我们看看为什么我们要这样做。
首先,应当记住,与对象相关的某些限制。 它们关心的并非可以做什么,而是应该如何做。 使用类来创建系统抽象允许我们隐藏其余代码的实际情况。 我们并不真正关心 C_Object_Bitmap 类应当怎么做才能在屏幕上显示图像。
现在请注意以下几点:如果图像不存在,或具有不同的格式,则其余代码和用户都不会被告知这一点。 我们若是得到一个空白区域,则指示存在错误。 因此,为了找出是否存在错误,我们应该检查 ObjectSetString 函数的返回。 如果返回 false,则表示无法加载图像,且可从对象列表中删除该对象。 但我们并未这样做。 为什么?
实际上,对我来说,这没有丝毫区别,因为放置在面板上的所有资产都拥有代表该资产的图像。 但还有一个原因,也许更重要。
如果您仔细观察,您会发现此表单中的代码不允许使用除位图文件之外的任何其它图像类型,例如,您不能将图像放置在透明背景上。 虽然有时透明背景本身很有用,但如果您尝试用现有代码实现它,您可能会得到一个显示非常奇怪信息的图像,而非所需的资产徽标。 故此,除上面代码所示之外,我们需要找到另一种呈现此类图像的方式。
其中一种方法已在文章让图表更有趣:添加背景中进行了介绍。 在那篇文章中,我们直接通过资源创建了图像。 虽然我也用了位图文件(因为它的内部代码构造模型更简单),但没有什么可以阻止您使用其它格式,例如具有动画或任何其它文件格式的 GIF 文件。 重点是,通过运用此方法,您可以创建和使用任何图像,包括透明背景,而这在本文前面讨论的代码中是不可能的。
因此,这种抽象非常重要:如果您想用不同的图像格式,甚至透明的背景图像,您不必修改任何代码 — 简单地修改该类的代码来实现您需要的东西。
出于这个原因,这段代码是作为一个类实现的,尽管它的代码相当紧凑。 通过使用类,我们可以从面板的其它部分隐藏此处发生的所有事情。
我们在 C_Object_Edit 类中还有一处变化。 我们看看这里有什么变化:
template < typename T > void Create(string szObjectName, color corTxt, color corBack, T InfoValue, string szFont = "Lucida Console", int iSize = 10) { C_Object_Base::Create(szObjectName, OBJ_EDIT); ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, szFont); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, iSize); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, corBack); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, corBack); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true); SetTextValue(szObjectName, InfoValue, corTxt); };
鉴于我们正在面板上实现更多信息,因此我们需要创建一个负责在图表上放置文本的类。 它比最初存在的要复杂一点。 现在我们将控制所用的字体类型及其字号。 因此,我们就能够在面板上放置比以前更多样化的东西。 这种增加的多样性也需要修改这个类的另一个函数。
template < typename T > void SetTextValue(string szObjectName, T InfoValue, color cor = clrNONE, const string szSufix = "") { color clr = (cor != clrNONE ? cor : (color)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR)); string sz0; if (typename(T) == "string") sz0 = (string)InfoValue; else if (typename(T) == "double") { clr = (cor != clrNONE ? cor : ((double)InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive)); sz0 = Terminal.ViewDouble((double)InfoValue < 0.0 ? -((double)InfoValue) : (double)InfoValue) + szSufix; }else sz0 = "?#?"; ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, sz0); ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr); };
您可以看到抽象是如何令一切变得不同:我们可以指出若干件事,比以前的版本更多。 那好,我们来看看上面的函数实际做了什么。
如果指定了颜色,则用此颜色。 如果未指定颜色,我们就用对象中已存在的颜色。 这非常有用,因为有时我们也许会简单地更改文本,而不更改颜色、或对象已有的任何其它内容。 在此阶段,我们检查指定的类型是否为字符串。 我们在运行时执行此操作,如此我们必须确保事情是正确的。
我们还可以使用主要用于指定资产价格的双精度数据类型。 如果在运行时检测到这种情况,我们将相应地设置值,并使用相应的颜色。 现在我们有一个有趣的事实,允许我们更多地讨论价值 — 我们将使用一个后缀,在调用期间用于通知。 如果有些不清楚,我们还格式化来指示这一点。 接下来,我们呈现文本,并调整其颜色。
通过简单地实现这些修改,我们已经可以创建如下所示的信息:
我们在左上角有一个图像。 资产代码显示在左上角。 右上角显示每日资产变化。 左下角包含一个箭头,指示资产变化是正数还是负数。 资产的最后一次报价显示在右下角。
但是为了呈现所有这些信息,我们需要在 C_Widget 类中实现一些修改。 我们看看现在已有什么。
新的 C_Widget 类
打开头文件 C_Widget.mqh 查看与系统的第一个基本版本相比发生了哪些变化。 我们来看看这些变化:
#include "Elements\C_Object_Edit.mqh" #include "Elements\C_Object_BackGround.mqh" #include "Elements\C_Object_BtnBitMap.mqh" //+------------------------------------------------------------------+ C_Terminal Terminal; //+------------------------------------------------------------------+ #define def_PrefixName "WidgetPrice" #define def_MaxWidth 160 //+------------------------------------------------------------------+ #define macro_MaxPosition (Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth) #define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + CharToString(A) + "#" + B)
在上面的代码中,我们已经连接了 C_Object_BtnBitMap.mqh 类。 此头文件支持以图像作为徽标。 此外,单元格宽度已更改,从而可以显示更多详细信息。
也许最有趣的事情是宏替换,它创建要在对象中用到的名称。 要注意它,因为它会被多次用到。
接下来,我们转到具有对象类中所有私密元素的声明部分:
class C_Widget { protected: enum EventCustom {Ev_RollingTo}; private : enum EnumTypeObject {en_Background = 35, en_Symbol, en_Price, en_Percentual, en_Icon, en_Arrow}; struct st00 { color CorBackGround, CorSymbol; int nSymbols, MaxPositionX; struct st01 { string szCode; }Symbols[]; }m_Infos; // ... Rest of the code
似乎并无变化。 但代码实际上已有一些变化。 我们快速浏览一下它们。 这些东西稍后会出现很多次,用到它们时您就能够更好地理解它们。 首先引人注目的是对象类型的枚举。 实际上,这并不表示我们正在创建什么类型的对象,而是指示该对象的用途是什么。 随着我们继续使用类代码,这将变得更加清晰。
您也许想知道为什么我们不用定义,而以枚举替代。 这是因为我们必须保证每个对象都是唯一的。 故此,我们需用枚举来保证这点。 如果我们使用定义,我们将冒着两个定义具有相同值的风险,那么对象就不是唯一的。 编程时请记住这个细节。
现在我们看一下系统中发生的关于内部流程的变化。 我们将从创建背景的函数开始。
void CreateBackGround(void) { C_Object_BackGround backGround; string sz0 = macro_ObjectName(en_Background, ""); backGround.Create(sz0, m_Infos.CorBackGround); backGround.Size(sz0, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH), Terminal.GetHeight()); }
此处,我们取第一个枚举。 故此,确实值得解释为什么枚举以值 35 开头,而非其它值。 但首先我们在这里完成最后一件重要的事情:当我们调整图表大小时,会生成 OnChartEvent 调用,这允许我们检查新大小,并相应地更新面板。
不过我们可以在某种意义上不必调整背景尺寸,因为面板高度始终是固定的。 稍后我们将看到我们从哪里定义此值。 但为了固定宽度,从而能够忽略面板宽度的变化,我们将采用的方法可能看起来不是很有效,但却可保证背景足够大。
现在我们看看为什么我们的枚举从 35 开始,即使我们可以取更小的值,但不能任意取值。
如果您查看生成对象名称的宏替换,您会注意到一些东西:
#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + CharToString(A) + "#" + B)
宏替换的第一个参数就是我们在类中定义的枚举。 枚举将被转换为相应的字符串,因此基于该值,我们得到一个字符,其可令名称唯一。 如果您查看 ASCII 字符表,您将看到第一个有效值是 32,它代表空格字符。 如此,我们就用 32 初始化该值,但取更小的数字初始化是没有意义的。
因此,发生这种情况只是因为我们要将数值转换为字符。 如果将数值转换为相应的字符串,我们可以用 0 初始化枚举。 但是在我们的例子中,当我们应对字符时,最小的适配值是 32。
现在我们看一下负责添加要所用对象的函数。
void AddSymbolInfo(const string szArg, const bool bRestore = false) { C_Object_Edit edit; C_Object_BtnBitMap bmp; string sz0; const int x = 9999; bmp.Create(sz0 = macro_ObjectName(en_Icon, szArg), szArg); bmp.PositionAxleX(sz0, x); bmp.PositionAxleY(sz0, 15); edit.Create(sz0 = macro_ObjectName(en_Symbol, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, szArg); edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, 10); edit.Size(sz0, 56, 16); edit.Create(sz0 = macro_ObjectName(en_Percentual, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, 0.0, "Lucida Console", 8); edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, 10); edit.Size(sz0, 50, 11); edit.Create(sz0 = macro_ObjectName(en_Arrow, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, "", "Wingdings 3", 10); edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, 26); edit.Size(sz0, 20, 16); edit.Create(sz0 = macro_ObjectName(en_Price, szArg), 0, m_Infos.CorBackGround, 0.0); edit.PositionAxleX(sz0, x); edit.PositionAxleY(sz0, 26); edit.Size(sz0, 60, 16); if (!bRestore) { ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10); m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg; m_Infos.nSymbols++; } }
在此,我们可以实现许多人都觉得稀奇的东西。 但对我来说,它只是一个笑话。 我喜欢参与和探索事物的可能性。 并且,我会问:“为什么我要这样做?
为什么我声明了一个常量变量,然后在调用中应用它? 原因是,正如我之前所说,我喜欢围绕语言的可能性开些玩笑,无论它是什么,以便发现它允许我们做什么。
事实上,您无需声明这样的常量就可做到。 您可以使用编译定义 (#define),或者只是将常量值放在每个调用当中。 结果是一样的。 无关于此,我们看一下函数代码的其余部分:您可以看到添加元素是为了让它们的名称是唯一的,即不能有两个同名的元素。 我们按以下顺序创建它们:BitMap,资产代码,每日百分比变化,指示变化方向的箭头,和当前资产价格。
请记住一件重要的事情:资源中使用的位图必须是 32 x 32 像素。 如果您使用其它尺寸,则必须调整其大小 — 但不是在这里。 稍后我们将看到这要在哪里完成。 但是您必须记住,所有其它对象的尺寸也应在此处调整,故此如果要令面板大于或小于附件中的面板,请在此处设置所需的值。请记住,这些函数中的每一个都与我们在调用之前声明的对象相关联。
现在我们来看看数据将如何实际呈现。
inline void UpdateSymbolInfo(int x, const string szArg) { C_Object_Edit edit; C_Object_BtnBitMap bmp; string sz0; double v0[], v1; ArraySetAsSeries(v0, true); if (CopyClose(szArg, PERIOD_D1, 0, 2, v0) < 2) return; v1 = ((v0[0] - v0[1]) / v0[(v0[0] > v0[1] ? 0 : 1)]) * 100.0; bmp.PositionAxleX(sz0 = macro_ObjectName(en_Icon, szArg), x); x += (int) ObjectGetInteger(Terminal.Get_ID(), sz0, OBJPROP_XSIZE); edit.PositionAxleX(macro_ObjectName(en_Symbol, szArg), x + 2); edit.PositionAxleX(sz0 = macro_ObjectName(en_Arrow, szArg), x + 2); edit.SetTextValue(sz0, CharToString(v1 >= 0 ? (uchar)230 : (uchar)232), (v1 >= 0 ? def_ColoPositive : def_ColorNegative)); edit.SetTextValue(sz0 = macro_ObjectName(en_Percentual, szArg), v1 , clrNONE, "%"); edit.PositionAxleX(sz0, x + 62); edit.SetTextValue(sz0 = macro_ObjectName(en_Price, szArg), v0[0] * (v1 >= 0 ? 1 : -1)); edit.PositionAxleX(sz0, x + 24); }
在此,我们确保数值序列始终从最新到最旧。一旦我们确定,我们就可以捕获收盘价。 请注意,我们正在执行每日读数,因为我们要在面板中显示每日变化。 另一点:如果返回的数据量小于预期,我们将立即退出函数,因为如果我们继续,我们最终会打断指标 — 相关信息将出现在 MetaTrader 5 工具箱中。 但我们不想因为错误而打断指标,这通常是暂时的。
现在注意以下计算。 它负责校正有关资产价格百分比变化的信息。 想来理解这种计算没有困难,因为它是一种很常见的计算,其为判定价格上涨或下跌的百分比。
接下来,我们将在画布上定位对象。 首先,我们放置图像。 请记住,当我们创建对象时,我们提过那个大小将用在其它地方。 是的,它就用在这此处,尽管我们需要的唯一信息是图像的宽度。 但是,正如我们之前所说,最好使用 32 x 32,因为如果您需要其它尺寸,尤其是更大的,则必须相应地调整其它数值。
如果尺寸较小,则不会有问题,因为代码将调整到较小的图像大小。 但如果您创建一个较大的面板,尤其是宽度更大,这意味着如果不调整 def_MaxWidth 的数值,信息将以非常奇怪的方式旋转。 在此,您应该在编译定义中增加其值。 但要小心:如果您设置的值太高,则与足够用的数值相比,您将不得不等待更长的时间才能重新显示该信息。
所有后续值将取决于图像宽度中包含的此值,故此它将判定这样的状况。 有一个时刻也许没有意义:我们有一个值被转换为一个字符,但角色本身不会出现在屏幕上。 此刻发生了什么?在此,我们根据每日变化创建箭头,指示价格是上涨还是下跌。 这可能看起来有点令人困惑,但一切操作都很好。 但请注意,我们所用的是字体是 Wingdings 3。 如果您要使用其它字体,则必须调整这些值才能正确显示。
还有一个有趣的观点。我们将字符串和双精度值发送到同一个函数。 我们期望两个不同的函数,但目标代码只包含一个。 此处该函数已重载。 但重载是由编译器和链接器创建的。 故此,我们必须在函数内部进行测试,但如果您仔细观察,您会发现我们有一个广泛用于表示百分比值的符号(%)。 现在进入 Edit 对象,查看百分比字符的添加位置,以及如何完成它。
为了完成此函数,我们有一处可以调整价格,如此即可在面板上正确显示其颜色。
所有这些更改都直接在函数中实现。 我们没有对系统的实际调用方式进行任何修改。 所有必要的更改都在 C_Widget 类中进行。 但是我们在指标代码中也有一些小的变化,所以我们来看看它们。
#property indicator_separate_window #property indicator_plots 0 #property indicator_height 32 //+------------------------------------------------------------------+ #include <Widget\Rolling Price\C_Widget.mqh> //+------------------------------------------------------------------+ input string user00 = "Config.cfg"; //Settings file input int user01 = -1; //Shift input int user02 = 10; //Pause in milliseconds input color user03 = clrWhiteSmoke; //Asset color input color user04 = clrBlack; //Price color //+------------------------------------------------------------------+ C_Widget Widget; //+------------------------------------------------------------------+ int OnInit() { if (!Widget.Initilize(user00, "Widget Price", user03, user04)) return INIT_FAILED; EventSetMillisecondTimer(user02); return INIT_SUCCEEDED; }
唯一值得一提的时刻是我们设置指标高度值的位置。 其它一切都与基本版相同。
我们可以在这里完成指标。 但您如何看待实现一些在交易期间附带的有用功能的想法? 我们将在下一篇文章中讨论这一点。
附加函数
您如何将交易的资产添加到面板中? 不仅如此。 我们更进一步。
这样如何:当您单击面板上显示的资产时,相关资产图表会即刻打开,如此您可以跟踪情况,并在看到趋势开始时入场交易。 且您能够以一种超级简单的方式完成所有这些操作,而无需键入任何内容!
这是一个很棒的主意,不是吗? 您一定认为这是一件超难做到的事情。 您需要成为一名在核物理或外星技术方面富有经验和知识的专业程序员。 并非如此。 这可以利用 MQL5 和 MetaTrader 5 平台轻松完成。
为此,我们需要向消息处理系统添加一小段代码。 具体操作如下:
void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { static int tx = 0; string szRet[]; switch (id) { // ... Internal code... case CHARTEVENT_OBJECT_CLICK: if (StringSplit(sparam, '#', szRet) == 2) { SymbolSelect(szRet[1], true); szRet[0] = ChartSymbol(Terminal.Get_ID()); if (ChartSetSymbolPeriod(Terminal.Get_ID(), szRet[1], PERIOD_CURRENT)) SymbolSelect(szRet[0], false); else SymbolSelect(szRet[1], false); } break; } }
当我们创建对象时,我们不只是以任何方式创建它们 — 我们使用非常特定的格式,因此,我们可以分析单击哪个对象,以及它与哪个资产相关联的情况。 如此,在这里我们能找出对象与哪个资产相关联。 为此,我们使用了一个非常有趣的函数。
StringSplit 可以根据数据的格式来拆分数据。 如此这般,我们将获得被点击的对象与哪个资产相关联的指示。 有基于此,我们指示 MetaTrader 5 平台打开相应的资产图表。
然而,若要完成此操作,资产必须存在于“市场报价”窗口当中。 故此,我们执行此行令交易品种出现在市场报价当中。然后,在从窗口中删除当前资产之前,我们捕获其代码,并尝试令单击的资产显示在窗口中。 如果我们成功了,我们会尝试从市场报价中删除图表上的资产。 但如果更改资产的尝试失败,我们尝试打开的资产将从市场观察中删除。
请注意使用面板时资产变化相关的几点。 首先,所需的资产将在与前一个资产相同的时间帧内打开。 您可以稍后更改时间帧,但最初它会采用相同的时间帧。 另一个同样重要的一点是,我们必须选择资产,如此面板上才会有合理数量的资产,因为再次在面板上显示它们需要花费大量时间。 随着时间帧或资产的每次变化,面板将始终从列表中的第一个资产开始。
以下视频演示了该系统在实践中的操作。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10963

