English Русский Español Deutsch 日本語 Português
preview
创建一个行情卷播面板:基本版

创建一个行情卷播面板:基本版

MetaTrader 5交易 | 21 二月 2023, 09:09
1 311 0
Daniel Jose
Daniel Jose

概述

有些人可能会发现某些平台内置的价格卷播面板在显示单个资产报价时非常炫酷。 如果您不知道我在说什么,请查看下面的动态图片:

在某些情况下,这些东西可能非常有用。 故此,在此我将展示如何在 MetaTrader 5 平台上利用内置的 MQL 来 100% 编程实现这样的元素。 许多人可能认为本文中的素材相当简单。 但我保证,如果您理解了这里提出的概念,您就能够创造更复杂的东西。

甚至,我还将撰写其它文章,在其中我将进一步开发此面板,如此对于那些想要实时交易和跟踪其它信息的人来说,它就会成为非常有用的工具。

我必须承认,这篇文章的想法是由这个社区的一位成员提出的。 这个想法的实现和开发非常有趣,对于许多人来说,它也是一个非常有用的资源,这就是为什么我决定展示如何为这样的面板创建代码。


计划

创建这样的面板并不太复杂。 实际上,与其它代码类型相比,它非常容易实现。 然而,在继续实现之前,我们要计划一些事情,这些事情能显著影响我们创建面板的方向。 由于这个想法是想有一个面板,从一开始就来显示资产及其价格且不会遇到太大困难;我将在这里展示如何创建一个非常基础的系统,但是它将作为更复杂、更精细系统的起点。

首先要考虑的是如何处理需在面板上显示的资产列表。 这会是包含一组预选资产的固定列表吗? 还是我们会在系统实现时一次插入一个品种?

这大概是最困难的部分,由于有时您也许想要拥有您感兴趣的资产,而在其它时候,您可能希望观察投资组合当中的资产。 故此,最好维护一份包含需在报价面板中显示的所有资产的文件。 如此,我们就使用包含需显示资产的文件。

现在出现了另一个问题:如何表示资源? 这似乎是一件小事,但细思实际上极其重要。 我们可以使用智能系统、脚本、指标或服务,尽管后者似乎是一个鲜见的解决方案。 至于我,我个人则喜欢使用服务。 不过,如果我们选它来实现面板,我们会有太多复杂的细节和困难,这令面板的开发成为一个复杂而耗时的过程。 因此,我们在实现该面板时仅限定在两个实用选项:将其放入 EA 或指标当中。 但为什么我们不能使用脚本呢?

原因很简单:如果用户决定更改时间帧,那么他最终将不得不关闭脚本。 因此,每次图表更改时,交易者都必须再次运行脚本。 正如我所说,这本应是一个 100% 的 MQL5 解决方案。 我们可以想一些利用外部编程解决方案的途径,但与我们原本目的不符。

因此,我们还剩下两个选项:智能系统和指标。 我不喜欢使用 EA 的想法,因为我更喜欢将 EA 用于其预期目的,即发送和控制订单。 因此,只剩下一个解决方案:使用指标。

还有其它问题需要考虑和规划,但我们已经可从这个初步计划启动。 那么,我们就开始吧。


基本原则

我们从创建指标文件开始:

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
//+------------------------------------------------------------------+

尽管指标代码是完全干净的,意即它不会做任何特别的事情,但我们对即将来到的未来已经有所猜测。 例如,我们将使用一个单独的指标窗口,我们需要处理比经典指标更多的事件,譬如通常不会出现在指标中的 OnTime。 不要忘记以下内容:我们根本不会绘制任何东西,因为指标将要创建的所有内容都会由指标完成。

我们通常从一些现成的代码开始,但在这篇特别的文章中,我打算以略有不同的详细程度展示所有内容,以便读者可以把这些素材作为研究和学习的来源。

您可能已经在考虑我们需要实现多少不同的事情才能令一切正常。 从某种意义上说,这是真的,但不会有太多事情。 首先要考虑的是如何管理图表。 为此,我们有一个类。 尽管许多人在我之前的文章中都见到过这个类,但在此它的样子会略有不同,因为我们需要用到的东西少了很多。 所以,我不会向所有不知道它的人展示 C_Terminal 类。 该类位于头文件 C_Terminal.mqh 当中。 它的代码非常简单 — 参见下文:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Terminal
{
//+------------------------------------------------------------------+
        private :
                struct st00
                {
                        long    ID;
                        int     Width,
                                Height,
                                SubWin;
                }m_Infos;
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+          
                void Init(const int WhatSub)
                        {
                                ChartSetInteger(m_Infos.ID = ChartID(), CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin = WhatSub, true);
                                Resize();
                        }
//+------------------------------------------------------------------+
inline long Get_ID(void)   const { return m_Infos.ID; }
inline int GetSubWin(void) const { return m_Infos.SubWin; }
inline int GetWidth(void)  const { return m_Infos.Width; }
inline int GetHeight(void) const { return m_Infos.Height; }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                m_Infos.Width = (int) ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
                                m_Infos.Height = (int) ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
                        }
//+------------------------------------------------------------------+
inline string ViewDouble(double Value)
                        {
                                Value = NormalizeDouble(Value, 8);
                                return DoubleToString(Value, ((Value - MathFloor(Value)) * 100) > 0 ? 2 : 0);
                        }
//+------------------------------------------------------------------+
                void Close(void)
                        {
                                ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin, false);
                        }
//+------------------------------------------------------------------+          
};
//+------------------------------------------------------------------+

是的,这是我们需要的完整代码。 虽然实际的类要大得多,但在此我只展示了必要的部分,因为我不想在文章里注水。

故此,对于那些不知道这个类做什么的人,我们快速浏览一下它的一些部分。 由于我们希望 MetaTrader 5 通知我们任何删除对象的尝试,我们需要在此处声明它,然后捕获我们所用窗口的大小。 此处,我们实际上创建了一个额外的抽象级别来帮助我们自己编程。

这不是强制性的,因此您能够以不同的途径实现它。 然而,感谢这个抽象级别,其内我们隐藏了所有未组装的内容,我们有一些调用来访问类数据。 在类结束时,我们应避免在指标开始删除对象时生成事件,而这也是我们使用这个函数的原因代码中有一处我们需要创建格式化 — 我们将在此执行,以便把终端相关的所有内容在一个类中维护。

到此处为止,事情很简单。 将来它们会变得越来越复杂,如此请小心。


实现主要对象

尽管看起来也许有些奇怪,但我们仅打算在面板的基本模型中用到两个对象。 由于我采用的是在 MQL5 社区中发布的 从头开始开发智能系统 系列中您所见的模型,故我将从该系列取用模型。 该系列提供了解释一切工作原理的所有详细信息,但我亦将在此处提供系统操作的一些简要说明。 因此,即使您不知道 MetaTrader 5 如何处理对象,您也不会迷路。

那么,就让我们从基准对象类开始,其代码如下:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "..\Auxiliar\C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
virtual void Create(string szObjectName, ENUM_OBJECT typeObj)
                        {
                                ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, Terminal.GetSubWin(), 0, 0);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                        };
//+------------------------------------------------------------------+
                void PositionAxleX(string szObjectName, int X)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X);
                        };
//+------------------------------------------------------------------+
                void PositionAxleY(string szObjectName, int Y, int iArrow = 0)
                        {
                                int desl = (int)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, (iArrow == 0 ? Y - (int)(desl / 2) : (iArrow == 1 ? Y : Y - desl)));
                        };
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        }
//+------------------------------------------------------------------+
                void Size(string szObjectName, int Width, int Height)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height);
                        };
//+------------------------------------------------------------------+
};

代码简单紧凑。 这提供了抽象级别,如此我们以后就可以少用许多代码。 在此,我们有一个虚函数,它负责以非常通用的方式创建任何对象。 但由于我们在这个基本模型中只用到一个对象,您可能会认为这个函数有点浪费时间。 这并不是真的,如果您查看 EA 的订单系统代码,您就会明白我在说什么。

我们还有另外两个函数在图表上定位对象。 我们还有一个更改对象颜色的函数,与对象创建功能一样,它也是虚拟的。 我们之所以需要它,因为有些对象具有复杂的颜色图案。 最后,我们有一个调整对象尺寸的函数

虽然看起来很愚蠢,但通过创建这个抽象级别在未来会于我们有所帮助,因为所有对象都能以独特的方式处理,无论对象是什么。 这提供了一些优势,但我们会留待另外的时间。 那么,我们来看看创建面板应选择哪个对象。 我们将选择 OBJ_EDIT。 其完整代码如下所示:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_ColorNegative       clrCoral
#define def_ColoPositive        clrPaleGreen
//+------------------------------------------------------------------+
class C_Object_Edit : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
                template < typename T >
                void Create(string szObjectName, color corTxt, color corBack, T InfoValue)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_EDIT);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, "Lucida Console");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, 10);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, corTxt);
                                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);
                                if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);
                        };
//+------------------------------------------------------------------+
                void SetTextValue(string szObjectName, double InfoValue, color cor = clrNONE)
                        {
                                color clr;
                                clr = (cor != clrNONE ? cor : (InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive));                                
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, Terminal.ViewDouble(InfoValue < 0.0 ? -(InfoValue) : InfoValue));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr);
                        };
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+
#undef def_ColoPositive
#undef def_ColorNegative
//+------------------------------------------------------------------+

仅此而已吗? 是的,仅此而已,尽管它与 EA 订单系统中所用的代码略有不同。 在此,我们已拥有所需的一切:一个放置双精度值的函数,这是我们在 MQL5 中最常用到的类型数据;创建编辑类型对象 Obj_Edit,还有也许会令初学者感到困惑的一件事。 仔细看看下面代码中的对象创建函数:

template < typename T >
void Create(string szObjectName, color corTxt, color corBack, T InfoValue)

实际上,编译器将这两行视为一行。 您明白这是怎么回事吗? 您认为我把事情复杂化了吗?

如此,当我们使用 'template < typename T > '(模板 < 类型名 T >)。 此处,T 可以替换为任何其它内容,前提是它符合当前的命名约定。 这种定义是重载的一种形式。 这很常见,当我们必须创建类似的函数时,这可接收不同的参数或数据类型。 这是极其普遍的。 如此,为了令我们此刻的生活更轻松,我们采用此语法。 这也许看起来很奇怪,但当您不想重写整个函数时,就会经常这样用,只是因为数据部分会有所不同,而函数的整个内部主体是相同的。

如果您注意,您会看到过程结束时只有一行,其中包含一行有趣的代码:

if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);

该代码执行以下操作:它检查 InfoValue 变量中通知的数据类型。 请注意,我说的是类型,而非数值,所以不要混淆这两个概念

如果类型是字符串,则将执行一段代码;如果类型不同,则将执行另一段代码,但这不是由编译器或链接器完成的。 分析通常是在运行时完成,因此我们应该显式告知哪些数据应予以处理,如此链接器才能正确设置进程。 这是由高亮显示的代码行完成的

因此,替代创建两个仅略有区别而大部分几乎雷同的函数,我们重载它,并在必要时进行调整,最终减少了工作量。

在 EA 代码中不需要这种方式,其内函数始终仅适配双精度的基本数据类型。 而现在,除了双精度,我们还能用字符串,我不想只为了实现两种类型就复制大量代码。

如果您想了解更多信息,请查看模板函数。 这些信息将帮助您了解为什么函数重载如此频繁地使用,以及如何避免仅仅因为所用类型不同,就要重写所有代码。

但在我们完成与对象相关的部分之前,请注意面板底部的对象。 我们需要创建一个背景。 您不会期望一切都能顺利进行但缺乏背景,是吗? 但别担心,这个代码非常简单。 查看如下代码:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
class C_Object_BackGround : public C_Object_Base
{
        public:
//+------------------------------------------------------------------+
                void Create(string szObjectName, color cor)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                                this.SetColor(szObjectName, cor);
                        }
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor);
                        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

这段代码简单明了,所以我认为它不需要任何解释。 它仅用于创建面板背景。 无论如何,我在这里展示它,方便有人想知道创建背景的代码是什么样子的。

至此,我们就可以结束本章节。 我们已经实现了对象,且拥有终端的支持结构,因此我们可以继续进行下一步。


实现主类

到目前为止,我们已经为这一步做好了准备,这是最令人兴奋的,因为在这里我们将令系统实际工作。 相关代码位于头文件 C_Widget.mqh 之中。 我们从如下所示的初始声明开始:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "Elements\C_Object_Edit.mqh"
#include "Elements\C_Object_BackGround.mqh"
//+------------------------------------------------------------------+
C_Terminal Terminal;
//+------------------------------------------------------------------+
#define def_PrefixName          "WidgetPrice"
#define def_NameObjBackGround	def_PrefixName + "BackGround"
#define def_MaxWidth            80
//+------------------------------------------------------------------+
#define def_CharSymbol          "S"
#define def_CharPrice           "P"
//+------------------------------------------------------------------+
#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() + A + "#" + B)
//+------------------------------------------------------------------+

在此,我们声明了我们实际需要的头文件,尽管还有其它文件。 但我们并不需要所有,因此这些头文件就足够了,它们已涵盖了所有其它文件。

我们还声明终端类,以便我们可用它来创建面板。 在此头文件 C_Widget.mqh 中,我们还有要用到的一些声明宏替换。 但要非常小心宏,因为它们必须以正确的方式加以使用。 只要我们正确使用它们,就不会有大问题,它们会为我们提供很大帮助。

一旦此操作完成后,我们声明具有初始变量的类。

class C_Widget
{
        protected:
                enum EventCustom {Ev_RollingTo};
        private :
                struct st00
                {
                        color   CorBackGround,
                                CorSymbol,
                                CorPrice;
                        int     nSymbols,
                                MaxPositionX;
                        struct st01
                        {
                                string szCode;
                        }Symbols[];
                }m_Infos;

这个枚举稍后会非常有用,尽管它不是必需的。拥有它非常有用,因为它会令事情变得更加抽象。 您得到的代码将更易于阅读和理解。 稍后我们将声明一个结构,它将帮助我们控制一些事情。 但是现在您不必担心它,只要知道它在这里,它是类的一个完全私密的部分,即没有外部代码能够访问它。

现在我们继续实际动作,其中第一个如下所示:

void CreateBackGround(void)
{
        C_Object_BackGround backGround;
                        
        backGround.Create(def_NameObjBackGround, m_Infos.CorBackGround);
        backGround.Size(def_NameObjBackGround, Terminal.GetWidth(), Terminal.GetHeight());
}

在此,我们实际创建面板背景。 请注意,我们会用到子窗口的整个区域。 我们把对象放置在这里,并用一种颜色填充所有内容。 因此,我们将获得统一的背景。 正如我在上一章节中提到的,我们正在创建某些抽象,它允许我们编程更少,更快地接收结果。 现在我们将继续讨论更复杂的事情。

void AddSymbolInfo(const string szArg, const bool bRestore = false)
        {
#define macro_Create(A, B, C)   {                                               \
                edit.Create(A, m_Infos.CorSymbol, m_Infos.CorBackGround, B);    \
                edit.PositionAxleX(A, def_MaxWidth * m_Infos.nSymbols);         \
                edit.PositionAxleY(A, C);                                       \
                edit.Size(A, def_MaxWidth - 1, 22);                             \
                                }
                        
                C_Object_Edit edit;

                macro_Create(macro_ObjectName(def_CharSymbol, szArg), szArg, 10);
                macro_Create(macro_ObjectName(def_CharPrice, szArg), 0.0, 32);
                if (!bRestore)
                {
                        ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10);
                        m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg;
                        m_Infos.nSymbols++;
                }
#undef macro_Create
        }

在此函数中,我声明了一个仅在此处使用的宏替换。 请注意,在完成之前,我删除了宏替换,因为它没有在函数之外的任何地方用到

在此,我们创建一个类型为 C_Object_Edit 的对象暂时定位它,并告知它应具有的大小。 所有这些都是在宏替换中完成的。 在这些位置上,我们用宏替换令代码易于阅读,因为整个过程实际上是相同的。 当然,数值存在问题,但函数是相同的,这就是我们使用宏替换的原因。 再有,打字更少,生产力更多。

现在我们进入一个重要的细节。 当用户删除不应删除的对象时,将调用相同的函数。 在这种情况下,将不会执行其它行。 但它们在正常创建过程中会被执行,其中我们首先分配内存,然后将品种名称放在分配的位置,并在下一次调用时递增它。 然后我们能够继续下一次调用。

代码中的下一段是以下有趣的函数:

inline void UpdateSymbolInfo(const int x, const string szArg)
{
        C_Object_Edit edit;
        string sz0 = macro_ObjectName(def_CharPrice, szArg);
        MqlRates Rate[1];
                                
        CopyRates(szArg, PERIOD_M1, 0, 1, Rate);                                
        edit.PositionAxleX(macro_ObjectName(def_CharSymbol, szArg), x);
        edit.SetTextValue(sz0, Rate[0].close, m_Infos.CorPrice);
        edit.PositionAxleX(sz0, x);
}

许多人认为我们需要全局级别的对象,但实际上在 MetaTrader 5 中,且使用 MQL5 时, 事实并非如此,因为所有创建的对象都可以根据需要进行操作。 若要找出对象的名称,检查品种图表上存在的所有对象列表的窗口。 因此,我们可以使用本地访问,并操作图表上存在的对象,前提是您知道它们的名称。

然后我们创建对象的名称以便能够操纵它。 出于方便起见,我们将使用宏替换。 在那之后还有另一件有趣的事情。 正常时,我们需要在“市场报价”窗口中拥有我们想要获取的资产。 但在我们的例子中,当我们创建一个面板时,必须在市场报价中打开并保留数百种资产,而这会令用户丧失使用它的动力。 为了避免这种情况,我们将采用另一种方法,但这种方法亦有其成本。 没有任何东西真是免费的。 其成本如下:在每次调用此函数期间,我们将复制最后一根柱线,来了解发生了什么。

之后,我们将所需的对象定位在正确的位置,并通知其数值来绘制对象。 但请记住,每次调用,我们在执行时都会有一小段延迟。 我们将在本文后面对此进行改进。

下一个函数如下所示:

bool LoadConfig(const string szFileConfig)
{
        int file;
        string sz0;
        bool ret;
                                
        if ((file = FileOpen("Widget\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                PrintFormat("Configuration file %s not found.", szFileConfig);
                return false;
        }
        m_Infos.nSymbols = 0;
        ArrayResize(m_Infos.Symbols, 30, 30);
        for (int c0 = 1; (!FileIsEnding(file)) && (!_StopFlag); c0++)
        {
                if ((sz0 = FileReadString(file)) == "") continue;
                if (SymbolExist(sz0, ret)) AddSymbolInfo(sz0); else
                {
                        FileClose(file);
                        PrintFormat("Ativo na linha %d não foi reconhecido.", c0);
                        return false;
                }
        }
        FileClose(file);
        m_Infos.MaxPositionX = macro_MaxPosition;
                
        return !_StopFlag;
}

在此,我们将读取一个文件,其中包含将在面板中用到的所有资产。 请注意,不需要扩展名 — 我只需指定文件所在的位置。 故此,您可以为文件指定任何名称,从而针对不同的东西提供不同的文件。

但您应该注意所用文件含有正确的数据,否则您可能会遇到一些麻烦。 在附件中,除了系统的完整代码之外,我还添加了一个文件来示意内部格式。 此文件内含当前 Ibovespa 指数(IBOV)中的所有成分资产。 使用此文件作为创建所有其它文件的基础。 此系统中采用相同的格式,并会在所有其它更新和改进中采用。

如果找到文件,且可打开,我们将执行一次调用,分配内存,并在数据到达时存储数据。 然后我们开始逐行读取,直到文件结束,或用户中断操作如果遇到任何一行为空,或不包含任何信息,则发起新的读取调用这是另一个重要时刻:仅当资产存在时才添加资产;如果不存在,则返回一个错误,指示它发生在哪一行。 错误消息将显示在“工具箱”窗口当中。 不再读取后续行,并返回错误。 最后,我们为将来配置重要信息,如此以后不必执行非必要的计算。

~C_Widget()
{
        Terminal.Close();
        ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
        ArrayFree(m_Infos.Symbols);
}

此函数是类析构函数。 当类关闭时,它会自动调用。 如果发生这种情况,整个系统将随之关闭,而在类中创建的所有对象均被删除分配的内存均被释放

在下面的代码中,我们有一个类初始化系统:

bool Initilize(const string szFileConfig, const string szNameShort, color corText, color corPrice, color corBack)
{
        IndicatorSetString(INDICATOR_SHORTNAME, szNameShort);
        Terminal.Init(ChartWindowFind());
        Terminal.Resize();
        m_Infos.CorBackGround = corBack;
        m_Infos.CorPrice = corPrice;
        m_Infos.CorSymbol = corText;
        CreateBackGround();

        return LoadConfig(szFileConfig);
}

除了以下几点,没有什么可说的,因为这里所用的所有内容我都已经解释过了。 在此,我们为指标定义一个短名称该名称作为参数通知,故此请注意这一点。 现在,这里的代码用于捕获指标所在子窗口的索引。 这很重要,因为对象 — 我们需要知道所处子窗口,否则我们可能会将对象放在错误的位置。

作为此头文件中的最后一个函数,我们要有消息传递系统。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Terminal.Resize();
                        m_Infos.MaxPositionX = macro_MaxPosition;
                        ChartRedraw();
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (StringSubstr(sparam, 0, StringLen(def_PrefixName)) == def_PrefixName) if (StringSplit(sparam, '#', szRet) == 2)
                        {
                                AddSymbolInfo(szRet[1], true);
                                ChartRedraw();
                        }else if (sparam == def_NameObjBackGround)
                        {
                                ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
                                CreateBackGround();
                                for (int c0 = 0; c0 < m_Infos.nSymbols; c0++) AddSymbolInfo(m_Infos.Symbols[c0].szCode, true);
                                ChartRedraw();
                        }
                        break;
        }
}

这些代码的大部分都非常简单:我们有两个由平台生成的事件要传递给指标进行处理,但我们还有一个事件类型,对许多人来说毫无意义,因为它是一个自定义事件。 这种类型的事件在某些项目类型中很常见,但在这里它更多地用于集中处理可能发生的消息或事件。 尽管许多人不理解这一点,但 MetaTrader 5 平台和 MQL5 语言是面向事件的,这意味着我们并不按程序化的方式工作,而是处理事件,并在事件发生时进行处理。

若要了解自定义事件是如何生成的,我们必须查看指标代码。 这就是为什么,在解释之前(虽然我相信你们中的许多人可能会发现难以准确理解这个事件),我们来查看指标代码,它现在具有的功能视图与我们在文章开头看到的已有不同。

#property copyright "Daniel Jose"
#property description "Program for a panel of quotes."
#property description "It creates a band that displays asset prices."
#property description "For details on how to use it visit:\n"
#property description "https://www.mql5.com/ru/articles/10941"
#property link "https://www.mql5.com/ru/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
#property indicator_height 45
//+------------------------------------------------------------------+
#include <Widget\Rolling Price\C_Widget.mqh>
//+------------------------------------------------------------------+
input string    user00 = "Config.cfg";  //Configuration file
input int       user01 = -1;            //Shift
input int       user02 = 60;            //Pause in milliseconds
input color     user03 = clrWhiteSmoke; //Asset color
input color     user04 = clrYellow;     //Price color
input color     user05 = clrBlack;      //Background color
//+------------------------------------------------------------------+
C_Widget Widget;
//+------------------------------------------------------------------+
int OnInit()
{
        if (!Widget.Initilize(user00, "Widget Price", user03, user04, user05))
                return INIT_FAILED;
        EventSetMillisecondTimer(user02);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
        EventChartCustom(Terminal.Get_ID(), C_Widget::Ev_RollingTo, user01, 0.0, "");
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Widget.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+

这在指标代码中罕有用到,其为指标窗口高度的规范。 但这不是重点。 请注意以下细节:

当用户为此参数定义数值时,我们得到的数据用作计时器。 十分确定的是,如果可能的话,我们应该避免在指标中使用 OnTime 事件。 但不幸的是,我们别无选择。 我们需要这个事件。 现在请注意,当 MetaTrader 5 平台触发 OnTime 事件时,它会生成 OnTime 事件调用。 在这个函数内,我们只有一行启动异步事件,这意味着我们不能确定何时调用代码。 这是一个自定义事件。

请注意,自定义事件内的参数不是随便的参数。 它们位于那里都有一个非常重要的原因:它们中的每一个都代表一件事,但最终我们将有一个 OnChartEvent 调用它将调用 C_Widget 类中的函数,该函数将处理事件生成的消息

现在注意以下几点:当我们调用 EventChartCustom 函数时,我们设置了一个事件,作为 OnChartEvent 函数的 ID。 此值将在消息处理函数中作为标识。 如果直接调用消息处理函数,则代码将是异步的,即我们将代码的其余部分设置为等待模式,从而等待消息处理函数的返回。 但如若我们调用 EventChartCustom,则代码不会处于等待模式。 这可避免我们不知道的持续时间阻塞其它指标。

我们通过 EventChartCustom 实现调用的事实还有另一个优点:此调用可以来自代码的任何位置。 无论我们从哪里调用它,ChartEvent 始终会触发并调用 OnChartEvent,并确保所需的执行。

这种方式也会用在另一篇文章当中,该文章涉及另一个但同样有趣的主题。 我现在不会谈论它,以便在文章发布之前保持神秘。

我希望这部分很清晰:自定义事件是如何生成的,以及为什么我使用自定义事件替代直接调用移动面板的代码。 现在我们回到包含此自定义面板移动事件处理的代码,请记住,用户在这里指定了一个参数,这对于移动非常重要

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;

上面代码中涉及的数学可能看起来令人困惑,但我正在此处所做的是依据用户提供的值把对象移动到一定距离处。 如果值为正,则从左向右移动对象;如果值为负,则对象从右向左移动;如果为零 — 它将保持在原地。 这个想法很简单,但计算在哪里,怎么看不到? 这就是为什么我说上面的代码似乎令人困惑。 计算是在这两行中执行。

您也许不完全理解它怎么可能,这么简单的计算是如何做到这一点的。 但如果您足够认真,您能看出我们用到限制。 当达到上限时,重新计算与其直接相关限制的位置。 我们采用闭环,如此当值达到某个点时,把它调整为从相反的限制点开始。 为了更好地理解,就好像您从 0 到 99 计数,且不能超出这些值,如果您尝试将 1 加到 99 会发生什么? 根据逻辑,我们会得到 100。

但在这种情况下不是。 在我们的例子中,我们会回到 0。 如果您尝试将 3 加到 98,您不会得到超过 99 的数值 — 您会得到 1。 这看起来很奇怪,但这就是它的工作原理。 当我们从 2 中减去 3 时,这同样适用 — 我们得到 99.... 听起来很疯狂😵 😵 😵...但这是计算机计数系统的基础。 如果您研究它,您会发现计算机不会计算无限的数字。 最大获得值有一定的极限,其适用在另一个加密领域,但那是另一回事。 

我们回到代码。 您应该试着理解我们刚刚讨论的内容,因为当我们来到 FOR 循环时,事情会变得更加奇怪。

我们在 FOR 循环中执行以下操作:我们不知道我们应该在哪里或多少时就结束,因为上述计算并没有告诉我们应该在屏幕上显示某些内容的位置。 为此,我们需要创建一个窗口,或者更确切地说,我们将使用图表窗口的限制来了解哪些应该显示或不应该显示

如果您不理解上面讨论的概念,这部分将非常混乱。 我们仅有的两个信息是:我们应该显示多少元素,以及当前正在使用哪个值。 根据这些信息,我们应该做其余的所有事情。 因此,我们从一个元素到另一个元素,始终从零元素开始,随着我们的进展,我们将每个元素的宽度累加到起始位置在某一点上,我们将在上轨线或下轨线超越限制。 一旦超越,我们就指示当前元素绘制位置的值应相应地进行调整。 一旦发生这种情况,我们将遇到位置偏差,如此信息将神奇地消失在屏幕的一侧,并开始在另一侧出现。

循环重复,直到指标关闭。 因此,无论我们拥有多少信息,所有信息都将出现在屏幕上。

这比用纯文本来完成更简单、更容易规划。 但尽管该技术非常相似,但大多数人往往在代码里使用矩阵,元素在其内移动,并且每个单元格已经具有明确定义的显示位置。 但在我们的案例中,这不会产生预期的结果,这就是为什么我不得不采用不同的方法,我们以纯数学方法来生成平滑和正确的移动。

另一个细节是我们应该避免使用大于 -1 或 1 的值,因为移动会有一半跳动,给人一种奇怪的印象。

在下面的视频中,您可以看到系统取自 IBOV(Ibovespa 指数)资产的数据。 这只是演示系统如何工作...




结束语

虽然这个系统看起来已经完全完工,但仍有改进的余地。 在下一篇文章中,我将向您展示如何在系统中进行这些改进。 请继续关注,因为更新即将来临。 附件包括本文的完整代码。 您可随心所欲地使用它。


本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10941

附加的文件 |
创建一个行情卷播面板:改进版 创建一个行情卷播面板:改进版
您如何看待复查我们的行情卷播面板基本版的主意? 我们改进面板要做的第一件事就是能够添加图像,例如资产徽标或其它图像,从而用户可以迅速、轻松地识别所示品种。
种群优化算法:蚁群优化(ACO) 种群优化算法:蚁群优化(ACO)
这次我将分析蚁群优化算法。 该算法非常有趣且复杂。 在本文中,我尝试创建一种新型的 ACO。
DoEasy. 控件 (第 25 部分): Tooltip WinForms 对象 DoEasy. 控件 (第 25 部分): Tooltip WinForms 对象
在本文中,我将开始开发 Tooltip(工具提示)控件,以及函数库的新图形基元。 自然而然地,并非每个元素都有工具提示,但每个图形对象都有设置它的能力。
帧分析器(Frames Analyzer)工具带来的时间片交易魔法 帧分析器(Frames Analyzer)工具带来的时间片交易魔法
什么是帧分析器(Frames Analyzer)? 这是适用于任意智能系统的一个插件模块,在策略测试器中、以及测试器之外进行参数优化期间,该工具在参数优化完成后立即读取测试创建的 MQD 文件、或数据库,并分析优化帧数据。 您能够与拥有帧分析器工具的其他用户共享这些优化结果,从而共同讨论结果。