English Русский 中文 Español Deutsch 日本語
Gráficos na biblioteca DoEasy (Parte 95): Controles de objetos gráficos compostos

Gráficos na biblioteca DoEasy (Parte 95): Controles de objetos gráficos compostos

MetaTrader 5Exemplos | 2 junho 2022, 13:45
299 0
Artyom Trishkin
Artyom Trishkin

Sumário


Ideia

Continuamos a desenvolver objetos gráficos compostos, isto é, objetos gráficos padrão feitos a partir de vários objetos e combinados em um objeto gráfico. Os objetos gráficos incluídos no objeto composto são definidos na biblioteca como objetos gráficos padrão estendidos. Esses objetos têm algumas propriedades extras e funcionalidades que lhes permitem quer seja incorporar outros objetos gráficos ou serem incorporados.
O desenvolvimento de um objeto gráfico composto requer uma funcionalidade que permita não apenas manter o objeto no lugar junto a outro objeto, como também ajustar sua localização quando o objeto pai for alterado ou movido.
No último artigo, começamos a criar manipuladores de eventos que tinham a ver com objetos gráficos compostos, geramos a remoção do objeto gráfico composto e começamos a desenvolver um manipulador para movê-lo.

Hoje vamos nos desviar um pouco do tópico anterior, que era mover um objeto gráfico composto. Em vez disso, vamos fazer um manipulador de eventos de alteração de gráfico que tem algum objeto gráfico composto, e vamos lidar com os objetos de controle do objeto gráfico composto.
Qual o motivo para isso? Planejamos a criação em tempo real de objetos gráficos compostos, isto é, tencionamos anexar um objeto dependente ao objeto de base quando arrastarmos o objeto dependente para o objeto de base. O objeto gráfico de base irá monitorar se outro objeto está se aproximando dele via mouse, e ativará o mecanismo de ancoragem se tal objeto se acercar a uma certa distância de um de seus pontos de ancoragem no gráfico. Também serão mostradas as linhas que conectam o ponto de ancoragem do objeto a ser anexado com o ponto de ancoragem do objeto de base, para, desse modo, sabermos a olho se o objeto arrastado está pronto para ser anexado ao objeto de base. Para fazer isso, precisamos ter um objeto-forma com um determinado tamanho em cada ponto de ancoragem do objeto gráfico. Tal tamanho determinará quando deverá ser ativado o mecanismo de ancoragem. Enquanto isso, umas linhas indicarão se os objetos estão prontos para interação. Embora tais formas sejam invisíveis em cada ponto de ancoragem do objeto gráfico, poderemos ver o tamanho da zona em questão apenas durante a depuração, ao ativar o desenho de retângulo ao longo das bordas da forma:

Além disso, a forma irá mostrar os pontos de ancoragem do objeto gráfico, e estes só aparecerão no momento em que o ponteiro do mouse entrar na área ativa da forma. Assim, moveremos e modificaremos o objeto gráfico estendido, não selecionando-o com um clique do mouse, senão passando o cursor do mouse sobre a área ativa da forma. Assim que movermos o cursor para a área ativa da forma (delineada por retângulos na figura acima), o ponto de ancoragem do objeto gráfico será assinalado (na figura acima é o ponto azul no centro do círculo) e se pegarmos a forma e começarmos a movê-la, o ponto de ancoragem correspondente do objeto gráfico se moverá após o cursor, modificando o próprio objeto e, respectivamente, o objeto gráfico composto.
Se, quando mantemos o botão do mouse pressionado, o cursor do mouse entra na área ativa da forma é porque estamos arrastando um objeto gráfico diferente sobre a forma, o que ativa o mecanismo de ancoragem de um objeto em outro. Assim, com estas formas, resolvemos vários problemas de uma só vez.

Hoje ao invés de anexar um objeto a outro (porque ainda não estamos prontos para começar a criar esta funcionalidade), criaremos as formas propriamente ditas, anexá-las-emos aos pontos de ancoragem do objeto gráfico e faremos um mecanismo para movê-las para as coordenadas dos pontos de ancoragem do objeto quando o gráfico mudar em posição e escala. Isto precisa ser feito porque, ao contrário da maioria dos objetos gráficos que têm coordenadas em valores de tempo/preço, o objeto-forma tem coordenadas em pixels.


Modificando as classes da biblioteca

Inserimos os índices das novas mensagens no arquivo \MQL5\Include\DoEasy\Data.mqh:

//--- CLinkedPivotPoint
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X,                // Not a single pivot point is set for the object along the X axis
   MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y,                // Not a single pivot point is set for the object along the Y axis
   MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE,             // The object is not attached to the basic graphical object
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ,       // Failed to create a data object for the X and Y pivot points
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X,            // Number of base object pivot points for calculating the X coordinate: 
   MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y,            // Number of base object pivot points for calculating the Y coordinate: 

//--- CGStdGraphObjExtToolkit
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA,     // Failed to change the size of the pivot point time data array
   MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA,    // Failed to change the size of the pivot point price data array
   MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM,   // Failed to create a form object to manage a pivot point
   
  };
//+------------------------------------------------------------------+

E incorporamos as mensagens que correspondem aos índices recém-adicionados:

//--- CLinkedPivotPoint
   {"Для объекта не установлено ни одной опорной точки по оси X","The object does not have any pivot points set along the x-axis"},
   {"Для объекта не установлено ни одной опорной точки по оси Y","The object does not have any pivot points set along the y-axis"},
   {"Объект не привязан к базовому графическому объекту","The object is not attached to the base graphical object"},
   {"Не удалось создать объект данных опорной точки X и Y.","Failed to create X and Y reference point data object"},
   {"Количество опорных точек базового объекта для расчёта координаты X: ","Number of reference points of the base object to set the X coordinate: "},
   {"Количество опорных точек базового объекта для расчёта координаты Y: ","Number of reference points of the base object to set the Y coordinate: "},
   
//--- CGStdGraphObjExtToolkit
   {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"},
   {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"},
   {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"},
   
  };
//+---------------------------------------------------------------------+


Alteramos a macro-substituição no o arquivo \MQL5\Include\DoEasy\Defines.mqh

#define CLR_DEFAULT                    (0xFF000000)               // Default symbol background color in the navigator

assim, vemos que se consegue entender melhor

#define CLR_MW_DEFAULT                 (0xFF000000)               // Default symbol background color in the Market Watch

Também alteramos a macro-substituição

#define NULL_COLOR                     (0x00FFFFFF)               // Zero for the canvas with the alpha channel

assim, vemos que se consegue entender melhor

#define CLR_CANV_NULL                  (0x00FFFFFF)               // Zero for the canvas with the alpha channel

Além disso, incluímos novas macro-substituições para definir valores padrão dos objetos-formas que criaremos hoje:

//--- Graphical object parameters
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Maximum value of an ID of a graphical object belonging to a program
#define CTRL_POINT_SIZE                (5)                        // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_FORM_SIZE                 (40)                       // Size of the control point form for managing graphical object pivot points
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+


Como os nomes das macro-substituições foram alterados, substituiremos os nomes desatualizados em todos os arquivos.

Basta pressionar a combinação de teclas Ctrl+Shift+H e inserir os seguintes valores nos campos e marcar as caixas como se mostra a continuação:


Em seguida, pressionamos o botão Replace in Files (Substituir em arquivos). O editor pesquisará em todas as pastas da biblioteca e as substituirá.

Faremos o mesmo para substituir "NULL_COLOR" por "CLR_CANV_NULL"

Tais nomes para macro-substituições são mais claros e não forçarão a lembrar mais tarde o propósito de cada uma delas (inseri CLR_DEFAULT por engano para definir fundo transparente de tela, e estive à procura por muito tempo do motivo de não ser transparente).

Além dessas mudanças, fiz pequenas alterações em todos os arquivos de descendentes dos objetos gráficos de base (adicionei uma vírgula no texto do método que exibe uma breve descrição do objeto no log):

//+------------------------------------------------------------------+
//| Display a short description of the object in the journal         |
//+------------------------------------------------------------------+
void CGStdArrowBuyObj::PrintShort(const bool dash=false,const bool symbol=false)
  {
   ::Print
     (
      (dash ? " - " : "")+this.Header(symbol)," \"",CGBaseObj::Name(),"\": ID ",(string)this.GetProperty(GRAPH_OBJ_PROP_ID,0),
      ", ",::TimeToString(CGBaseObj::TimeCreate(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
  }
//+------------------------------------------------------------------+

Isto é puramente uma melhoria a nível de "design".
Foi feito em todos os arquivos da pasta \MQL5\Include\DoEasy\Objects\Graph\Standard\, e podemos vê-lo por si mesmos nos arquivos anexos ao artigo.


Classe ferramentas do objeto gráfico padrão estendido

Bem, vamos começar a criar ferramentas para trabalhar com objetos gráficos estendidos. Tratar-se-á de uma classe na qual escreveremos os métodos necessários para criar e trabalhar com objetos-forma. Cada objeto gráfico estendido conterá um ponteiro para o objeto de dada classe. Se necessário (caso se trate do objeto de base dentro do objeto gráfico composto), o objeto da classe será criado dinamicamente quando o objeto estendido for gerado e apagado quando for eliminado.

Os parâmetros do objeto gráfico de base (suas coordenadas, tipo, nome, etc.) serão passados para o objeto, e as coordenadas do objeto de base e o ajuste das coordenadas do objeto-forma serão monitorados dentro do objeto. Assim, os objetos-formas eventualmente permitirão controlar o objeto de base.

Mas antes de mais nada...

Criamos uma nova pasta Extend\ no diretório da biblioteca \MQL5\Include\DoEasy\Objects\Graph\. Em seguida, geramos um novo arquivo CGStdGraphObjExtToolkit.mqh da classe CGStdGraphObjExtToolkit herdada da classe de base CObject para construir a biblioteca padrão MQL5:

//+------------------------------------------------------------------+
//|                                      CGStdGraphObjExtToolkit.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\..\Graph\Form.mqh"
//+------------------------------------------------------------------+
//| Extended standard graphical                                      |
//| object toolkit class                                             |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
  }

O arquivo da classe deve se anexado ao arquivo da classe.

Na seção privada da classe, declararemos as variáveis que armazenarão todas as propriedades necessárias do objeto de base,
as propriedades para construir formas, a lista para armazená-las e métodos para criar objetos-formas e retornar suas coordenadas de tela:

//+------------------------------------------------------------------+
//| Extended standard graphical                                      |
//| object toolkit class                                             |
//+------------------------------------------------------------------+
class CGStdGraphObjExtToolkit : public CObject
  {
private:
   long              m_base_chart_id;           // Base graphical object chart ID
   int               m_base_subwindow;          // Base graphical object chart subwindow
   ENUM_OBJECT       m_base_type;               // Base object type
   string            m_base_name;               // Base object name
   int               m_base_pivots;             // Number of base object reference points
   datetime          m_base_time[];             // Time array of base object reference points
   double            m_base_price[];            // Price array of base object reference points
   int               m_base_x;                  // Base object X coordinate
   int               m_base_y;                  // Base object Y coordinate
   int               m_ctrl_form_size;          // Size of forms for managing reference points
   int               m_shift;                   // Shift coordinates for adjusting the form location
   CArrayObj         m_list_forms;              // List of form objects for managing reference points
//--- Create a form object on a base object reference point
   CForm            *CreateNewControlPointForm(const int index);
//--- Return X and Y screen coordinates of the specified reference point of the graphical object
   bool              GetControlPointCoordXY(const int index,int &x,int &y);
public:

Usaremos matrizes para armazenar as coordenadas de preço e tempo, já que um objeto gráfico pode ter vários pontos de ancoragem e, consequentemente, as coordenadas de cada ponto serão armazenadas nas células correspondentes da matriz. A coordenada do primeiro ponto é armazenada pelo índice de matriz 0, a coordenada do segundo ponto, pelo índice de matriz 1, o terceiro, pelo índice de matriz 2, etc.
Precisamos do deslocamento das coordenadas da forma para posicionar a mesma exatamente no centro do ponto de ancoragem do objeto, e este deslocamento será metade de seu tamanho. Se definirmos o tamanho da forma como um múltiplo de dois, por exemplo, 10, ele será corrigido em mais 1, ou seja, 11. Isto é necessário para que a forma possa ser localizada exatamente no centro do ponto de ancoragem do objeto gráfico e um dos seus lados não seja um pixel maior do que o lado oposto.
Armazenaremos todas as formas que criarmos na lista de formas, e as acessaremos a partir de tal lista usando um ponteiro. Precisamos do método de cálculo das coordenadas da tela para saber em onde colocar a forma, de modo que ela se posicionará exatamente no ponto de ancoragem do objeto gráfico.

Na seção pública da classe, declaramos todos os métodos necessários para trabalhar com a classe:

public:
//--- Set the parameters of the base object of a composite graphical object
   void              SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                const long base_chart_id,const int base_subwindow,
                                const int base_pivots,const int ctrl_form_size,
                                const int base_x,const int base_y,
                                const datetime &base_time[],const double &base_price[]);
//--- Set the base object (1) time, (2) price, (3) time and price coordinates
   void              SetBaseObjTime(const datetime time,const int index);
   void              SetBaseObjPrice(const double price,const int index);
   void              SetBaseObjTimePrice(const datetime time,const double price,const int index);
//--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates
   void              SetBaseObjCoordX(const int value)                        { this.m_base_x=value;                          }
   void              SetBaseObjCoordY(const int value)                        { this.m_base_y=value;                          }
   void              SetBaseObjCoordXY(const int value_x,const int value_y)   { this.m_base_x=value_x; this.m_base_y=value_y; }
//--- (1) Set and (2) return the size of the form of pivot point management control points
   void              SetControlFormSize(const int size);
   int               GetControlFormSize(void)                          const { return this.m_ctrl_form_size;                }
//--- Return the pointer to the pivot point form by (1) index and (2) name
   CForm            *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CForm            *GetControlPointForm(const string name,int &index);
//--- Return the number of the base object pivot points
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
//--- Create form objects on the base object pivot points
   bool              CreateAllControlPointForm(void);
//--- Remove all form objects from the list
   void              DeleteAllControlPointForm(void);
   
//--- Event handler
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);

//--- Constructor/destructor
                     CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name,
                                             const long base_chart_id,const int base_subwindow,
                                             const int base_pivots,const int ctrl_form_size,
                                             const int base_x,const int base_y,
                                             const datetime &base_time[],const double &base_price[])
                       {
                        this.m_list_forms.Clear();
                        this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price);
                        this.CreateAllControlPointForm();
                       }
                     CGStdGraphObjExtToolkit(){;}
                    ~CGStdGraphObjExtToolkit(){;}
  };
//+------------------------------------------------------------------+

No construtor da classe limpamos a lista de objetos-formas, nas variáveis da classe definimos todos os valores do objeto de base (passados nos parâmetros do construtor) necessários para a operação da mesma, e em cada ponto de ancoragem do objeto gráfico de base criamos os objetos-formas necessários para controlar o objeto de base e interagir com ele.

Método que define os parâmetros do objeto de base de um objeto gráfico composto:

//+------------------------------------------------------------------+
//| Set the base object parameters of the                            |
//| composite graphical object                                       |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const string base_name,
                                         const long base_chart_id,const int base_subwindow,
                                         const int base_pivots,const int ctrl_form_size,
                                         const int base_x,const int base_y,
                                         const datetime &base_time[],const double &base_price[])
  {
   this.m_base_chart_id=base_chart_id;       // Base graphical object chart ID
   this.m_base_subwindow=base_subwindow;     // Base graphical object chart subwindow
   this.m_base_type=base_type;               // Base object type
   this.m_base_name=base_name;               // Base object name
   this.m_base_pivots=base_pivots;           // Number of base object reference points
   this.m_base_x=base_x;                     // Base object X coordinate
   this.m_base_y=base_y;                     // Base object Y coordinate
   this.SetControlFormSize(ctrl_form_size);  // Size of forms for managing reference points
   
   if(this.m_base_type==OBJ_LABEL            || this.m_base_type==OBJ_BUTTON  ||
      this.m_base_type==OBJ_BITMAP_LABEL     || this.m_base_type==OBJ_EDIT    ||
      this.m_base_type==OBJ_RECTANGLE_LABEL  || this.m_base_type==OBJ_CHART)
      return;
   
   if(::ArraySize(base_time)==0)
     {
      CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArraySize(base_price)==0)
     {
      CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY);
      return;
     }
   if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      return;
     }
   if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots)
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      return;
     }
   for(int i=0;i<this.m_base_pivots;i++)
     {
      this.m_base_time[i]=base_time[i];      // Time (i) of the base object pivot point
      this.m_base_price[i]=base_price[i];    // Price (i) of the base object pivot point
     }
  }
//+------------------------------------------------------------------+

Todos os valores de propriedades do objeto gráfico de base necessários para a operação de classe são passados para o método. Em seguida, verificamos o tipo do objeto de base, e se for um objeto que não é construído usando coordenadas preço/tempo, saímos do método, porque por enquanto não trabalhamos com tais objetos e simplesmente não vamos processá-los. Seguidamente, verificamos as dimensões das matrizes de coordenadas do objeto de base passadas para o método, e se elas forem zero (matriz vazia passada para o método), notificamos e saímos do método. Após isso, redimensionamos as matrizes de coordenadas internas de acordo com as passadas, e se as mesmas não foram redimensionadas, notificamos e saímos. No final, basta copiarmos as matrizes de entrada elemento por elemento para as matrizes internas.

Método que define o tamanho dos pontos de ancoragem:

//+------------------------------------------------------------------+
//|Set the size of reference points for managing pivot points        |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetControlFormSize(const int size)
  {
   this.m_ctrl_form_size=(size>254 ? 255 : size<5 ? 5 : size%2==0 ? size+1 : size);
   this.m_shift=(int)ceil(m_ctrl_form_size/2)+1;
  }
//+------------------------------------------------------------------+

O tamanho de forma é passado para o método. Caso o tamanho seja maior do que 254, vamos defini-lo como 255 (um valor ímpar), se o tamanho for menor do que 5, vamos defini-lo como 5 (este será o tamanho mínimo da forma), caso contrário, i.e., se o tamanho for múltiplo de dois, vamos adicionar um a ele e usá-lo, se nenhum dos valores verificados for verdadeiro, vamos usar o tamanho passado para o método.
Depois, calculamos o valor de deslocamento (porque a forma deve ficar de modo que o ponto de ancoragem do objeto gráfico esteja exatamente em seu centro, e para isso precisamos subtrair o valor de deslocamento do valor da coordenada). Dividimos o tamanho calculado da forma por dois, pegamos o número inteiro mais próximo do topo e acrescentamos um.

Método que define a coordenada de tempo do objeto de base:

//+------------------------------------------------------------------+
//| Set the time coordinate of the base object                       |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTime(const datetime time,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
  }
//+------------------------------------------------------------------+

O valor de tempo do ponto de ancoragem e o índice do ponto de ancoragem do objeto são passados para o método. Se o índice for maior que o número de pontos de ancoragem no objeto, notificamos sobre a solicitação fora da matriz e saímos. Como resultado, o valor de tempo passado para o método é inserido na matriz de tempo na célula correspondente ao índice.
O método é necessário para especificar o tempo do objeto de base no objeto da classe quando ele muda.

Método que define a coordenada de preço do objeto de base:

//+------------------------------------------------------------------+
//| Set the coordinate of the base object price                      |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjPrice(const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

O método é idêntico ao anterior, exceto que aqui nós escrevemos o preço do ponto de ancoragem indicado pelo índice do objeto de base na matriz de preços da classe.

Método que define as coordenadas de hora e preço do objeto de base:

//+------------------------------------------------------------------+
//| Set the time and price coordinates of the base object            |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const datetime time,const double price,const int index)
  {
   if(index>this.m_base_pivots-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY);
      return;
     }
   this.m_base_time[index]=time;
   this.m_base_price[index]=price;
  }
//+------------------------------------------------------------------+

O método é idêntico aos dois anteriores, mas define o preço e o tempo nas matrizes da classe.

Método que retorna as coordenadas X e Y do ponto de ancoragem do objeto gráfico em coordenadas da tela:

//+------------------------------------------------------------------+
//| Return the X and Y coordinates of the specified pivot point      |
//| of the graphical object in screen coordinates                    |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y)
  {
   switch(this.m_base_type)
     {
      case OBJ_LABEL             :
      case OBJ_BUTTON            :
      case OBJ_BITMAP_LABEL      :
      case OBJ_EDIT              :
      case OBJ_RECTANGLE_LABEL   :
      case OBJ_CHART             :
        x=this.m_base_x;
        y=this.m_base_y;
        break;
      default:
        if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y))
          {
           x=0;
           y=0;
           return false;
          }
     }
   return true;
  }
//+------------------------------------------------------------------+

O método recebe o índice do ponto de ancoragem do objeto gráfico de base - para o qual devem ser obtidas as coordenadas da tela (em pixels do canto superior esquerdo da mesma) - e as duas variáveis por referência onde serão escritas as coordenadas da forma. Se o objeto já estiver em coordenadas de tela, vamos devolvê-las.
Se o objeto estiver localizado nas coordenadas preço/tempo, devemos calculá-las usando a função ChartTimePriceToXY(), e, se ele não conseguir converter as coordenadas em coordenadas de tela, teremos que defini-las como zero e retornar false.
Como resultado, retornamos true
.

Método que retorna um ponteiro para a forma do ponto de ancoragem segundo o nome:

//+------------------------------------------------------------------+
//| Return the pointer to the pivot point form by name               |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index)
  {
   index=WRONG_VALUE;
   for(int i=0;i<this.m_list_forms.Total();i++)
     {
      CForm *form=this.m_list_forms.At(i);
      if(form==NULL)
         continue;
      if(form.Name()==name)
        {
         index=i;
         return form;
        }
     }
   return NULL;
  }
//+------------------------------------------------------------------+

O método recebe o nome da forma que procuramos e uma variável por referência que armazenará o índice da forma encontrada na lista de objetos-formas.
Em um loop ao longo da lista de objetos-formas, recuperamos o próximo objeto e, se seu nome coincide com a forma encontrada, escrevemos o índice do loop em uma variável e retornamos um ponteiro para o objeto encontrado. No final do loop, retornamos NULL, o que significa que a forma não foi encontrada e o índice terá o valor -1 antes do início do loop.

Método que cria um objeto de forma no ponto de ancoragem do objeto de base:

//+------------------------------------------------------------------+
//| Create a form object on a base object reference point            |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index)
  {
   string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (string)index : "X");
   CForm *form=this.GetControlPointForm(index);
   if(form!=NULL)
      return NULL;
   int x=0, y=0;
   if(!this.GetControlPointCoordXY(index,x,y))
      return NULL;
   return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
  }
//+------------------------------------------------------------------+

O método recebe o índice do ponto de ancoragem em que é necessário criar o objeto-forma.
Criamos um nome para o objeto-forma, que deverá consistir no nome do objeto de base + abreviação de "ToolKit Pivot Point" (_TKPP) + índice de pontos de ancoragem. Ao criar a descrição de índice, verificamos seu valor e, se ele é menor que o número de pontos de ancoragem do objeto de base (o cálculo começa de zero), utilizamos a representação de string do índice passado para o método. Caso contrário, usamos "X". Por que isso é necessário? Mais adiante, poderemos prender objetos dependentes não apenas aos pontos de ancoragem do objeto de base, mas também entre eles. Além disso, para mover todo o objeto, precisaremos criar uma forma de controle no meio da linha do objeto de base, para o objeto inteiro se mover atrás dela. Por isso, o nome da forma deve prever imediatamente a possibilidade de criar uma forma não só para os pontos de ancoragem, mas também para outros.
Em seguida, com o índice passado para o método verificamos se a forma está na lista, e se ela já estiver (o ponteiro para o objeto-forma não é igual a NULL), retornaremos NULL.
Em seguida, usamos índice do ponto de ancoragem para convertê-lo em coordenadas de tela, e retornamos o resultado da criação de objeto-forma com base nessas coordenadas. Ao fazer isso, subtraímos os deslocamentos de ambas as coordenadas para posicionar com precisão o centro da forma no ponto de ancoragem.
Poderíamos ter apenas estabelecido um valor para o centro da forma, mas estipulamos na biblioteca que o ponto de ancoragem de todos as formas deveria permanecer inalterado, em seu canto superior-esquerdo. Assim, sempre que necessário, utilizamos um deslocamento para posicionar objetos-formas.

Método que cria objetos-formas em pontos de ancoragem do objeto de base:

//+------------------------------------------------------------------+
//| Create form objects on the base object pivot points              |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void)
  {
   bool res=true;
   //--- In the loop by the number of base object pivot points
   for(int i=0;i<this.m_base_pivots;i++)
     {
      //--- Create a new form object on the current pivot point corresponding to the loop index
      CForm *form=this.CreateNewControlPointForm(i);
      //--- If failed to create the form, inform of that and add 'false' to the final result
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=false;
        }
      //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result
      if(!this.m_list_forms.Add(form))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         delete form;
         res &=false;
        }
      //--- Set all the necessary properties for the created form object
      form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Object is created programmatically
      form.SetActive(true);                     // Form object is active
      form.SetMovable(true);                    // Movable object
      form.SetActiveAreaShift(0,0,0,0);         // Object active area - the entire form
      form.SetFlagSelected(false,false);        // Object is not selected
      form.SetFlagSelectable(false,false);      // Object cannot be selected by mouse
      form.Erase(CLR_CANV_NULL,0);              // Fill in the form with transparent color and set the full transparency
      //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location
      form.DrawCircle((int)floor(form.Width()/2),(int)floor(form.Height()/2),CTRL_POINT_SIZE,clrDodgerBlue);   // Draw a circle in the form center
      form.DrawCircleFill((int)floor(form.Width()/2),(int)floor(form.Height()/2),2,clrDodgerBlue);             // Draw a point in the form center
      form.Done();                              // Save the initial form object state (its appearance)
     }
   //--- Redraw the chart for displaying changes (if successful) and return the final result
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

Toda a lógica está totalmente descrita nos comentários sobre o código. Em resumo: em um loop pelo número de pontos de ancoragem do objeto de base, criamos um novo objeto-forma para cada ponto de ancoragem, o adicionamos à lista de objetos-formas e definimos em cada forma as coordenadas do ponto de ancoragem correspondente do objeto de base. Em cada forma, um círculo no centro e um ponto são desenhados, para indicar que este é o objeto de controle para o ponto de ancoragem do objeto de base.

Conforme planejado, estes objetos serão inicialmente invisíveis e só aparecerão quando o cursor do mouse entrar na área da forma. Mas, por enquanto, vamos torná-los visíveis, para testar seu comportamento quando o gráfico mudar. Já nos artigos seguintes os faremos como pretendido, i.e., ocultos e aparecendo quando o cursor entrar em sua área ativa, que será o tamanho total do objeto-forma.

Método que remove todos os objetos-formas da lista:

//+------------------------------------------------------------------+
//| Remove all form objects from the list                            |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(void)
  {
   this.m_list_forms.Clear();
  }
//+------------------------------------------------------------------+

Nós apenas usamos o método Clear(), que limpa completamente a lista.

Vamos processar os eventos dos objetos-formas de acordo com o evento ocorrido no manipulador de eventos:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      for(int i=0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         int x=0, y=0;
         if(!this.GetControlPointCoordXY(i,x,y))
            continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
      ::ChartRedraw(this.m_base_chart_id);
     }
  }
//+------------------------------------------------------------------+

No momento, estamos processando apenas o evento de alteração do gráfico. Em um loop por todos os objetos-forma, obtemos a próxima forma da lista e, se não for possível obter suas coordenadas de tela de acordo com o ponto de ancoragem no qual ela é desenhada, passamos para a próxima. Definimos as novas coordenadas de tela da forma e a atualizamos. No final do loop, redesenhamos o gráfico para exibir as alterações.

Como o objeto ferramentas do objeto gráfico padrão estendido será armazenado no objeto da própria classe de objeto gráfico padrão, precisamos modificar essa classe no arquivo \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.

Primeiro, incluímos no arquivo os arquivos de classe do objeto-forma e o recém-criado objeto gráfico padrão estendido:

//+------------------------------------------------------------------+
//|                                                 GStdGraphObj.mqh |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
#property strict    // Necessary for mql4
//+------------------------------------------------------------------+
//| Include files                                                    |
//+------------------------------------------------------------------+
#include "..\GBaseObj.mqh"
#include "..\..\..\Services\Properties.mqh"
#include "..\..\Graph\Form.mqh"
#include "..\..\Graph\Extend\CGStdGraphObjExtToolkit.mqh"
//+------------------------------------------------------------------+
//| Class of the dependent object pivot point data                   |
//+------------------------------------------------------------------+

Declaramos um ponteiro para o objeto ferramentas do objeto gráfico padrão estendido na seção privada dentro da classe do objeto gráfico padrão abstrato:

//+------------------------------------------------------------------+
//| The class of the abstract standard graphical object              |
//+------------------------------------------------------------------+
class CGStdGraphObj : public CGBaseObj
  {
private:
   CArrayObj         m_list;                                            // List of subordinate graphical objects
   CProperties      *Prop;                                              // Pointer to the property object
   CLinkedPivotPoint m_linked_pivots;                                   // Linked pivot points
   CGStdGraphObjExtToolkit *ExtToolkit;                                 // Pointer to the extended graphical object toolkit
   int               m_pivots;                                          // Number of object reference points
//--- Read and set (1) the time and (2) the price of the specified object pivot point
   void              SetTimePivot(const int index);
   void              SetPricePivot(const int index);
//--- Read and set (1) color, (2) style, (3) width, (4) value, (5) text of the specified object level
   void              SetLevelColor(const int index);
   void              SetLevelStyle(const int index);
   void              SetLevelWidth(const int index);
   void              SetLevelValue(const int index);
   void              SetLevelText(const int index);
//--- Read and set the BMP file name for the "Bitmap Level" object. Index: 0 - ON, 1 - OFF
   void              SetBMPFile(const int index);

public:

Vamos escrever um método que retorne um ponteiro para o objeto ferramentas na seção pública:

public:
//--- Set object's (1) integer, (2) real and (3) string properties
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value)     { this.Prop.Curr.SetLong(property,index,value);    }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value)    { this.Prop.Curr.SetDouble(property,index,value);  }
   void              SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value)    { this.Prop.Curr.SetString(property,index,value);  }
//--- Return object’s (1) integer, (2) real and (3) string property from the properties array
   long              GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)          const { return this.Prop.Curr.GetLong(property,index);   }
   double            GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)           const { return this.Prop.Curr.GetDouble(property,index); }
   string            GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index)           const { return this.Prop.Curr.GetString(property,index); }
//--- Set object's previous (1) integer, (2) real and (3) string properties
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value);    }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value);  }
   void              SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value);  }
//--- Return object’s (1) integer, (2) real and (3) string property from the previous properties array
   long              GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index)      const { return this.Prop.Prev.GetLong(property,index);   }
   double            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index)       const { return this.Prop.Prev.GetDouble(property,index); }
   string            GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index)       const { return this.Prop.Prev.GetString(property,index); }
   
//--- Return (1) itself, (2) properties and (3) the change history
   CGStdGraphObj    *GetObject(void)                                       { return &this;            }
   CProperties      *Properties(void)                                      { return this.Prop;        }
   CChangeHistory   *History(void)                                         { return this.Prop.History;}
   CGStdGraphObjExtToolkit *GetExtToolkit(void)                            { return this.ExtToolkit;  }
//--- Return the flag of the object supporting this property
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property)  { return true;             }
   virtual bool      SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property)  { return true;             }

Declaramos o manipulador de eventos do objeto gráfico na seção pública da classe, definimos o ponteiro padrão para objeto ferramentas como NULL no construtor e verificamos a validade do ponteiro no destruidor da classe e apagamos todas as formas do objeto ferramentas e, em seguida, o próprio objeto:

private:
//--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object
   void              SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object
   void              SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to);
   void              SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to);
//--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property
   void              SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier);
   void              SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier);
   void              SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier);

public:
//--- Event handler
   void              OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
//--- Default constructor
                     CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; }
//--- Destructor
                    ~CGStdGraphObj()
                       {
                        if(this.Prop!=NULL)
                           delete this.Prop;
                        if(this.ExtToolkit!=NULL)
                          {
                           this.ExtToolkit.DeleteAllControlPointForm();
                           delete this.ExtToolkit;
                          }
                       }
protected:
//--- Protected parametric constructor
                     CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                                   const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                                   const ENUM_GRAPH_OBJ_BELONG belong,
                                   const ENUM_GRAPH_OBJ_SPECIES species,
                                   const long chart_id, const int pivots,
                                   const string name);
                     
public:
//+--------------------------------------------------------------------+ 
//|Methods of simplified access and setting graphical object properties|
//+--------------------------------------------------------------------+

No bloco de métodos para acesso simplificado e configuração de propriedades do objeto gráfico, escrevemos um método que retorne o número de pontos de ancoragem do objeto gráfico:

public:
//+--------------------------------------------------------------------+ 
//|Methods of simplified access and setting graphical object properties|
//+--------------------------------------------------------------------+
//--- Number of object reference points
   int               Pivots(void)                  const { return this.m_pivots;                                                          }
//--- Object index in the list
   int               Number(void)                  const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM,0);                            }
   void              SetNumber(const int number)         { this.SetProperty(GRAPH_OBJ_PROP_NUM,0,number);                                 }


No construtor paramétrico protegido, verificamos o tipo de elemento gráfico e, se for um estendido, criamos um novo objeto ferramentas e armazenamos um ponteiro para ele na variável ExtToolkit. Inicializamos o objeto ferramentas no final da listagem do construtor:

//+------------------------------------------------------------------+
//| Protected parametric constructor                                 |
//+------------------------------------------------------------------+
CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type,
                             const ENUM_GRAPH_ELEMENT_TYPE elm_type,
                             const ENUM_GRAPH_OBJ_BELONG belong,
                             const ENUM_GRAPH_OBJ_SPECIES species,
                             const long chart_id,const int pivots,
                             const string name)
  {
//--- Create the property object with the default values
   this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL);
   this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL);
//--- Set the number of pivot points and object levels
   this.m_pivots=pivots;
   int levels=(int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS);

//--- Set the property array dimensionalities according to the number of pivot points and levels
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels);
   this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,2);
   
//--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits
   this.m_type=obj_type;
   this.SetName(name);
   CGBaseObj::SetChartID(chart_id);
   CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type));
   CGBaseObj::SetTypeElement(elm_type);
   CGBaseObj::SetBelong(belong);
   CGBaseObj::SetSpecies(species);
   CGBaseObj::SetSubwindow(chart_id,name);
   CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS));
   
//--- Save the integer properties inherent in all graphical objects but not present in the current one
   this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,0,CGBaseObj::ChartID());                // Chart ID
   this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,0,CGBaseObj::SubWindow());               // Chart subwindow index
   this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject());            // Graphical object type (ENUM_OBJECT)
   this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement());   // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE)
   this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong());                   // Graphical object affiliation
   this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species());                 // Graphical object species
   this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0);                                      // Graphical object group
   this.SetProperty(GRAPH_OBJ_PROP_ID,0,0);                                         // Object ID
   this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0);                                    // Base object ID
   this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0);                                        // Object index in the list
   this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false);                         // Flag of storing the change history
   this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name());                        // Base object name
   
//--- Save the properties inherent in all graphical objects and present in a graphical object
   this.PropertiesRefresh();
   
//--- Save basic properties in the parent object
   this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0);
   this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0);
   this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0);
   this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0);
   this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0);

//--- Initialize the extended graphical object toolkit
   if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      datetime times[];
      double prices[];
      if(::ArrayResize(times,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA);
      if(::ArrayResize(prices,this.Pivots())!=this.Pivots())
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA);
      for(int i=0;i<this.Pivots();i++)
        {
         times[i]=this.Time(i);
         prices[i]=this.Price(i);
        }
      this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices);
      this.ExtToolkit.CreateAllControlPointForm();
      this.SetFlagSelected(false,false);
      this.SetFlagSelectable(false,false);
     }

//--- Save the current properties to the previous ones
   this.PropertiesCopyToPrevData();
  }
//+-------------------------------------------------------------------+

Ao inicializar o objeto ferramentas, primeiro declaramos as matrizes das propriedades de tempo e preço, redimensionamo-las para ajustá-las ao número de pontos de ancoragem do objeto gráfico e escrevemos valores de preço e tempo - a partir dos pontos de ancoragem do objeto correspondentes ao índice - nestas propriedades em um loop.
Em seguida, chamamos o método de inicialização do objeto ferramentas e passamos a ele os parâmetros necessários do objeto gráfico e as matrizes de propriedades de preço e tempo recém-preenchidas. Após a inicialização, chamamos o método de criação de objetos-formas nos pontos de ancoragem do objeto gráfico e, finalmente, definimos o status do objeto não selecionado para o objeto gráfico e proibimos que ele seja selecionado com o mouse.

No método que verifica mudanças nas propriedades do objeto, no bloco de código que manipula o objeto gráfico padrão estendido, incorporamos um bloco de código para mover os pontos de controle (objetos-formas) para as novas coordenadas quando a localização dos pontos de ancoragem do objeto gráfico padrão estendido mudar:

//+------------------------------------------------------------------+
//| Check object property changes                                    |
//+------------------------------------------------------------------+
void CGStdGraphObj::PropertiesCheckChanged(void)
  {
   CGBaseObj::ClearEventsList();
   bool changed=false;
   int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i;
      if(!this.SupportProperty(prop)) continue;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

   begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i;
      if(!this.SupportProperty(prop)) continue;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j))
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }

   begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL;
   for(int i=begin; i<end; i++)
     {
      ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i;
      if(!this.SupportProperty(prop)) continue;
      for(int j=0;j<Prop.CurrSize(prop);j++)
        {
         if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME)
           {
            changed=true;
            this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name());
           }
        }
     }
   if(changed)
     {
      for(int i=0;i<this.m_list_events.Total();i++)
        {
         CGBaseEvent *event=this.m_list_events.At(i);
         if(event==NULL)
            continue;
         ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam());
        }
      if(this.AllowChangeHistory())
        {
         int total=HistoryChangesTotal();
         if(this.CreateNewChangeHistoryObj(total<1))
            ::Print
              (
               DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==0 ? "0-1" : (string)total),
               ": ",this.HistoryChangedObjTimeChangedToString(total-1)
              );
        }
      //--- If subordinate objects are attached to the base one (in a composite graphical object)
      if(this.m_list.Total()>0)
        {
         //--- In the loop by the number of added graphical objects,
         for(int i=0;i<this.m_list.Total();i++)
           {
            //--- get the next graphical object,
            CGStdGraphObj *dep=m_list.At(i);
            if(dep==NULL)
               continue;
            //--- get the data object of its pivot points,
            CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
            if(pp==NULL)
               continue;
            //--- get the number of coordinate points the object is attached to
            int num=pp.GetNumLinkedCoords();
            //--- In the loop by the object coordinate points,
            for(int j=0;j<num;j++)
              {
               //--- get the number of coordinate points of the base object for setting the X coordinate
               int numx=pp.GetBasePivotsNumX(j);
               //--- In the loop by each coordinate point for setting the X coordinate,
               for(int nx=0;nx<numx;nx++)
                 {
                  //--- get the property for setting the X coordinate, its modifier
                  //--- and set it in the object selected as the current one in the main loop
                  int prop_from=pp.GetPropertyX(j,nx);
                  int modifier_from=pp.GetPropertyModifierX(j,nx);
                  this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
                 }
               //--- Get the number of coordinate points of the base object for setting the Y coordinate
               int numy=pp.GetBasePivotsNumY(j);
               //--- In the loop by each coordinate point for setting the Y coordinate,
               for(int ny=0;ny<numy;ny++)
                 {
                  //--- get the property for setting the Y coordinate, its modifier
                  //--- and set it in the object selected as the current one in the main loop
                  int prop_from=pp.GetPropertyY(j,ny);
                  int modifier_from=pp.GetPropertyModifierY(j,ny);
                  this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
                 }
              }
            dep.PropertiesCopyToPrevData();
           }
         //--- Move reference control points to new coordinates
         if(ExtToolkit!=NULL)
           {
            for(int i=0;i<this.Pivots();i++)
              {
               ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
              }
            ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
            long   lparam=0;
            double dparam=0;
            string sparam="";
            ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
         //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes
         ::ChartRedraw(m_chart_id);
        }
      //--- Save the current properties as the previous ones<
      this.PropertiesCopyToPrevData();
     }
  }
//+------------------------------------------------------------------+

Se movermos um dos pontos de ancoragem de um objeto gráfico ou o objeto inteiro, as coordenadas da tela de seus pontos de ancoragem mudarão. Por conseguinte, precisamos mover os objetos-formas da classe ferramentas para as novas coordenadas para que eles se situem no lugar. Por isso, aqui primeiro passamos as novas coordenadas do objeto gráfico (em um loop pelo número de pontos de ancoragem para as coordenadas preço/tempo e, separadamente, as coordenadas em pixels) para o objeto ferramentas, e depois chamamos o manipulador de evento do objeto ferramentas, passando o identificador do evento de mudança do gráfico para ele. Isto fará com que o manipulador de eventos do objeto ferramentas recalcule as coordenadas da tela de todas as formas e as mova para um novo local, de acordo com as novas coordenadas de preço e tempo do objeto gráfico.

Vamos corrigir um erro no método que adiciona um objeto gráfico padrão subordinado à lista. O objeto gráfico subordinado adicionado muda suas propriedades, portanto as novas propriedades precisam ser definidas imediatamente como as anteriores, para que não sejam gerados novos eventos de mudança para estes objetos gráficos quando clicamos sobre ele:

//+------------------------------------------------------------------+
//| Add a subordinate standard graphical object to the list          |
//+------------------------------------------------------------------+
bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj)
  {
   //--- If the current object is not an extended one, inform of that and return 'false'
   if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
     {
      CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ);
      return false;
     }
   //--- If failed to add the pointer to the passed object into the list, inform of that and return 'false'
   if(!this.m_list.Add(obj))
     {
      CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST);
      return false;
     }
   //--- Object added to the list - set its number in the list,
   //--- name and ID of the current object as the base one
   //--- set the flags of object availability and selection
   //--- and the graphical element type - standard extended graphical object
   obj.SetNumber(this.m_list.Total()-1);
   obj.SetBaseName(this.Name());
   obj.SetBaseObjectID(this.ObjectID());
   obj.SetFlagSelected(false,false);
   obj.SetFlagSelectable(false,false);
   obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED);
   obj.PropertiesCopyToPrevData();
   return true;
  }
//+------------------------------------------------------------------+


Manipulador de eventos do objeto gráfico padrão abstrato:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
      return;
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      if(ExtToolkit==NULL)
         return;
      for(int i=0;i<this.Pivots();i++)
        {
         ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
        }
      ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
      ExtToolkit.OnChartEvent(id,lparam,dparam,sparam);
     }
  }
//+------------------------------------------------------------------+

Até agora, o manipulador trata apenas do evento de alteração do gráfico.

Se o objeto não for estendido, saímos do manipulador. Caso o evento de alteração do gráfico esteja confirmado, verificamos se o ponteiro do objeto ferramentas do objeto gráfico padrão estendido é válido. Se não tiverem sido criadas nenhumas ferramentas, saímos. Em seguida, em um loop pelo número de pontos de ancoragem do objeto gráfico, definimos novas coordenadas de preço/tempo do objeto gráfico no objeto ferramentas. A seguir, definimos suas novas coordenadas de tela e chamamos o manipulador de eventos do objeto ferramentas, objeto esse onde todas as formas permanecerão na nova coordenada de tela calculada a partir das novas coordenadas de preço/tempo que acabaram de ser passadas para o objeto ferramentas.


Ao remover o objeto gráfico padrão estendido do gráfico, precisamos remover o objeto ferramentas do gráfico caso tal objeto tenha sido criado para o objeto gráfico. Processamos a exclusão dos objetos gráficos do gráfico na classe-coleção de elementos gráficos no arquivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

No método que trata da exclusão de objetos gráficos estendidos, incorporamos um bloco de código para a exclusão de todos os objetos-formas do objeto ferramentas:

//+------------------------------------------------------------------+
//| Handle the removal of extended graphical objects                 |
//+------------------------------------------------------------------+
void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj)
  {
   if(obj==NULL)
      return;
   //--- Save the ID of the graphical object chart and the number of subordinate objects in its list
   long chart_id=obj.ChartID();
   int total=obj.GetNumDependentObj();
   //--- If the list of subordinate objects is not empty (this is the base object)
   if(total>0)
     {
      CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit();
      if(toolkit!=NULL)
        {
         toolkit.DeleteAllControlPointForm();
        }
      //--- In the loop, move along all dependent objects and remove them
      for(int n=total-1;n>WRONG_VALUE;n--)
        {
         //--- Get the next graphical object
         CGStdGraphObj *dep=obj.GetDependentObj(n);
         if(dep==NULL)
            continue;
         //--- If failed to remove it from the chart, display the appropriate message in the journal
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
        }
      //--- Upon the loop completion, update the chart to display the changes and exit the method
      ::ChartRedraw(chart_id);
      return;
     }
   //--- If this is a subordinate object
   else if(obj.BaseObjectID()>0)
     {
      //--- Get the base object name and its ID
      string base_name=obj.BaseName();
      long base_id=obj.BaseObjectID();
      //--- Get the base object from the graphical object collection list
      CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id);
      if(base==NULL)
         return;
      //--- get the number of dependent objects in its list
      int count=base.GetNumDependentObj();
      //--- In the loop, move along all its dependent objects and remove them
      for(int n=count-1;n>WRONG_VALUE;n--)
        {
         //--- Get the next graphical object
         CGStdGraphObj *dep=base.GetDependentObj(n);
         //--- If failed to get the pointer or the object has already been removed from the chart, move on to the next one
         if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name()))
            continue;
         //--- If failed to delete the graphical object from the chart,
         //--- display the appropriate message in the journal and move on to the next one
         if(!::ObjectDelete(dep.ChartID(),dep.Name()))
           {
            CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
            continue;
           }
        }
      //--- Remove the base object from the chart and from the list
      if(!::ObjectDelete(base.ChartID(),base.Name()))
         CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART);
     }
   //--- Update the chart for displaying the changes
   ::ChartRedraw(chart_id);
  }
//+------------------------------------------------------------------+

Neste ponto tudo é simples. Recuperamos do objeto um ponteiro para seu objeto ferramentas do objeto gráfico estendido e, se o ponteiro for válido, chamamos o método para apagar todas as formas criadas do objeto ferramentas do objeto gráfico estendido padrão que discutimos anteriormente.

Adicionamos o tratamento de alterações do gráfico para objetos gráficos padrão estendidos no manipulador de eventos da classe-coleção de elementos gráficos:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj=NULL;
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Calculate the chart ID
      //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID
      //--- If the event ID corresponds to a user event, the chart ID is received from lparam
      //--- Otherwise, the chart ID is assigned to -1
      long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param);
      //--- Get the object, whose properties were changed or which was relocated,
      //--- from the collection list by its name set in sparam
      obj=this.GetStdGraphObject(sparam,chart_id);
      //--- If failed to get the object by its name, it is not on the list,
      //--- which means its name has been changed
      if(obj==NULL)
        {
         //--- Let's search the list for the object that is not on the chart
         obj=this.FindMissingObj(chart_id);
         //--- If failed to find the object here as well, exit
         if(obj==NULL)
            return;
         //--- Get the name of the renamed graphical object on the chart, which is not in the collection list
         string name_new=this.FindExtraObj(chart_id);
         //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart,
         //--- and send an event with the new name of the object to the control program chart
         if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name());
        }
      //--- Update the properties of the obtained object
      //--- and check their change
      obj.PropertiesRefresh();
      obj.PropertiesCheckChanged();
     }
//--- Handle chart changes for extended standard objects
   if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
     {
      CArrayObj *list=this.GetListStdGraphObjectExt();
      if(list!=NULL)
        {
         for(int i=0;i<list.Total();i++)
           {
            obj=list.At(i);
            if(obj==NULL)
               continue;
            obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
  }
//+------------------------------------------------------------------+

Aqui: se o evento de alteração do gráfico tiver sido confirmado, obtemos uma lista de todos os objetos gráficos padrão estendidos e, em um loop pelo seu número, obtemos o próximo objeto e chamamos seu manipulador de eventos, passando para ele o valor do evento "Gráfico alterado".


Teste

Vamos pegar o EA do artigo anterior, para efetuar o teste.
E vamos salvá-lo na nova pasta \MQL5\Experts\TestDoEasy\Part95\ com o novo nome TestDoEasyPart95.mq5.

A única coisa que mudou foram os nomes dos objetos marcadores de preço anexados ao objeto de base linha de tendência. Simplesmente, no gerenciador de eventos do EA, no bloco de criação de objetos gráficos compostos, adicionamos o texto "Ext" aos nomes dos objetos subordinados, de modo que os nomes correspondam ao tipo de objetos gráficos como estendidos:

   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      //--- Get the chart click coordinates
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         //--- Get the right point coordinates for a trend line
         datetime time2=iTime(Symbol(),PERIOD_CURRENT,1);
         double price2=iOpen(Symbol(),PERIOD_CURRENT,1);
         
         //--- Create the "Trend line" object
         string name_base="TrendLineExt";
         engine.CreateLineTrend(name_base,0,true,time,price,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID and pass its properties to the journal
         CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
         
         //--- Create the "Left price label" object
         string name_dep="PriceLeftExt";
         engine.CreatePriceLabelLeft(name_dep,0,false,time,price);
         //--- Get the object from the list of graphical objects by chart name and ID and
         CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line left point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0);
         
         //--- Create the "Right price label" object
         name_dep="PriceRightExt";
         engine.CreatePriceLabelRight(name_dep,0,false,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID and
         dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line right point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1);
        }
     }
   engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);

O que vamos testar? Vamos criar um objeto gráfico composto. Quando for criado, objetos-formas serão definidos para seus pontos de ancoragem.
Esses objetos-formas têm coordenadas em pixels a partir do canto superior esquerdo da tela. Assim, se movermos o gráfico, essas coordenadas da tela devem ser recalculadas para que os objetos fiquem nos pontos de ancoragem correspondentes do objeto gráfico. Verificaremos isto.

Compilamos o Expert Advisor e o iniciamos no gráfico:


Vemos que os objetos permanecem em seu lugar quando o gráfico muda. Mas eles atrasam muito.
Ao excluir o objeto gráfico, os objetos-formas pertencentes a ele também são excluídos.

O que fazer com tanto atraso? Em princípio, nunca precisaremos ver seu movimento ao vivo, uma vez que essas formas sempre estarão ocultas quando o gráfico for movido (elas agora são exibidas para controlar a reação ao evento). Já a linha do próprio objeto gráfico se moverá quando esses objetos-formas forem movidos com o mouse. Qualquer interação com as formas será realizada em um gráfico imóvel. Por isso, o resultado visto pode muito bem ser satisfatório. Especialmente considerando que a atualização do gráfico não é feita a cada iteração do loop, mas somente após sua conclusão. No entanto, para aliviar a carga, podemos controlar o fim de alteração do gráfico, e só então exibir as alterações e mostrar o objeto (e só se o cursor estiver na área ativa do objeto-forma, quando deve ser visível).

O que vem a seguir?

No próximo artigo, continuaremos trabalhando com eventos de objetos gráficos compostos.

Todos os arquivos da versão atual da biblioteca, bem como os do EA de teste e o indicador de controle de eventos do gráfico para MQL5 estão anexados abaixo. Você pode baixá-los e testar tudo por conta própria. Se você tiver dúvidas, comentários e sugestões, você pode colocá-los nos comentários do artigo.

Ir para o sumário

*Artigos desta série:

Gráficos na biblioteca DoEasy (Parte 93): Preparando a funcionalidade para criar objetos gráficos compostos
Gráficos na biblioteca DoEasy (Parte 94): Objetos gráficos compostos, movimentação e eliminação

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/10387

Arquivos anexados |
MQL5.zip (4207.98 KB)
Desenvolvendo um EA de negociação do zero (Parte 17): Acessando dados na WEB (III) Desenvolvendo um EA de negociação do zero (Parte 17): Acessando dados na WEB (III)
Como obter dados da WEB para serem usados em um EA. Então vamos por as mãos na massa, ou melhor começar a codificar um sistema alternativo.
Desenvolvendo um EA de negociação do zero (Parte 16): Acessando dados na WEB (II) Desenvolvendo um EA de negociação do zero (Parte 16): Acessando dados na WEB (II)
Como levar os dados da WEB para dentro de um EA . O caminho para fazer isto não é tão obvio, ou melhor dizendo, tão simples a ponto de você conseguir fazer, sem de fato conhecer e entender todos os recursos que estão presentes no MetaTrader 5.
Como desenvolver sistemas baseados em médias móveis Como desenvolver sistemas baseados em médias móveis
Existem muitas maneiras diferentes de filtrar os sinais gerados por qualquer estratégia. Provavelmente, a mais simples delas consiste no uso de uma média móvel. Vamos falar sobre isso neste artigo.
Desenvolvendo um EA de negociação do zero( Parte 15): Acessando dados na WEB (I) Desenvolvendo um EA de negociação do zero( Parte 15): Acessando dados na WEB (I)
Como ter acesso a dados na WEB dentro do MetaTrader 5. Na WEB temos diversos sites e locais onde uma grande e vasta quantidade de informações estão disponíveis e ficam acessíveis a aqueles que sabem onde procurar e como melhor utilizar estas informações.