Outras classes na biblioteca DoEasy (Parte 70): extensão da funcionalidade e atualização automática da coleção de objetos-gráficos

9 julho 2021, 10:21
Artyom Trishkin
0
271

Sumário


Ideia

No último artigo criamos uma coleção de objetos-gráficos. Agora, cada gráfico do símbolo aberto no terminal é representado por um objeto-gráfico. Cada objeto-gráfico possui um conjunto de objetos-janelas nos quais estão localizados os objetos-indicadores de janela. Qualquer objeto-gráfico tem pelo menos um objeto-janela, que é a janela principal do gráfico. Todas as outras janelas dos indicadores podem ser adicionadas ou removidas da lista de janelas do gráfico. Colocamos todo este conjunto de objetos na coleção de objetos-gráficos.

Testes detalhados da coleção de objetos-gráficos do artigo anterior revelaram alguns problemas no que diz respeito à adição de novas janelas à janela principal do gráfico. Vamos consertar isso hoje. Além disso, adicionaremos uma nova funcionalidade aos objetos-gráficos, nomeadamente a navegação na janela do gráfico do símbolo, a criação de capturas de tela das janelas e o salvamento/carregamento de modelos no gráfico.

Além das melhorias planejadas, hoje geraremos o rastreamento automático de alguns eventos ocorridos com gráficos no terminal do cliente e com janelas de objetos-gráficos - adição/exclusão de um gráfico existente do símbolo (objeto-gráfico), adição/exclusão de um indicador para o objeto-gráfico e adição/exclusão de um indicador à janela de gráfico.


Aprimorando as classes da biblioteca

Vamos a isso. Em primeiro lugar (como já é de costume) para o arquivo \MQL5\Include\DoEasy\Data.mqh escrevemos os índices das novas mensagens:

   MSG_CHART_OBJ_CHART_WINDOW,                        // Main chart window
   MSG_CHART_OBJ_CHART_SUBWINDOW,                     // Chart subwindow
   MSG_CHART_OBJ_CHART_SUBWINDOWS_NUM,                // Subwindows
   MSG_CHART_OBJ_INDICATORS_MW_NAME_LIST,             // Indicators in the main chart window
   MSG_CHART_OBJ_INDICATORS_SW_NAME_LIST,             // Indicators in the chart window
   MSG_CHART_OBJ_INDICATOR,                           // Indicator
   MSG_CHART_OBJ_INDICATORS_TOTAL,                    // Indicators
   MSG_CHART_OBJ_WINDOW_N,                            // Window
   MSG_CHART_OBJ_INDICATORS_NONE,                     // No indicators
   MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ,              // Failed to receive the chart window object
   MSG_CHART_OBJ_SCREENSHOT_CREATED,                  // Screenshot created
   MSG_CHART_OBJ_TEMPLATE_SAVED,                      // Chart template saved
   MSG_CHART_OBJ_TEMPLATE_APPLIED,                    // Template applied to chart
  
//--- CChartObjCollection
   MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION,        // Chart collection
   MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ,  // Failed to create a new chart object
   MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART,         // Failed to add a chart object to the collection
   MSG_CHART_COLLECTION_ERR_CHARTS_MAX,               // Cannot open new chart. Number of open charts at maximum
  
  };
//+------------------------------------------------------------------+

e os textos das mensagens correspondentes aos índices recém-adicionados:

   {"Главное окно графика","Main chart window"},
   {"Подокно графика","Chart subwindow"},
   {"Подокон","Subwindows"},
   {"Индикаторы в главном окне графика","Indicators in the main chart window"},
   {"Индикаторы в окне графика","Indicators in the chart window"},
   {"Индикатор","Indicator"},
   {"Индикаторов","Indicators total"},
   {"Окно","Window"},
   {"Отсутствуют","No indicators"},
   {"Не удалось получить объект-окно графика","Failed to get the chart window object"},
   {"Скриншот создан","Screenshot created"},
   {"Шаблон графика сохранён","Chart template saved"},
   {"Шаблон применён к графику","Template applied to the chart"},
   
//--- CChartObjCollection
   {"Коллекция чартов","Chart collection"},
   {"Не удалось создать новый объект-чарт","Failed to create new chart object"},
   {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"},
   {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"},
   
  };
//+---------------------------------------------------------------------+


Como hoje faremos algumas funcionalidades adicionais dos objetos-gráficos, funcionalidades essas que incluirão a criação de capturas de tela e o uso de modelos, precisaremos especificar as pastas de armazenamento para tais capturas de tela e modelos, assim como a extensão do nome do arquivo padrão (e, portanto, o formato do arquivo da imagem salva) das capturas de tela. Os tipos de arquivo disponíveis para salvar as capturas de tela podem ser *.gif, *.png e *.bmp.

Adicionamos estas novas substituições de macros ao arquivo \MQL5\Include\DoEasy\Defines.mqh:

//--- Data parameters for file operations
#define DIRECTORY                      ("DoEasy\\")               // Library directory for storing object folders
#define RESOURCE_DIR                   ("DoEasy\\Resource\\")     // Library directory for storing resource folders
#define SCREENSHOT_DIR                 ("DoEasy\\ScreenShots\\")  // Library directory for storing screenshot folders
#define TEMPLATE_DIR                   ("DoEasy\\")               // Library directory for storing template folders
#define FILE_EXT_GIF                   (".gif")                   // GIF image file name extension
#define FILE_EXT_PNG                   (".png")                   // PNG image file name extension
#define FILE_EXT_BMP                   (".bmp")                   // BMP image file name extension
#define SCREENSHOT_FILE_EXT            (FILE_EXT_PNG)             // Chart screenshot file format (extension: .gif, .png and .bmp can be used)
//--- Symbol parameters

Deve-se notar que as pastas de armazenamento das capturas de tela e as dos modelos no terminal são diferentes.

As capturas de tela são salvas na pasta (diretório de dados do Terminal) \MQL5\Files\

Mas os modelos são salvos na pasta (Diretório de dados do terminal)\ MQL5\Profiles\Templates\

Assim, a adição das substituições de macro especificadas ao nome do arquivo tornará o armazenamento dos arquivos da biblioteca mais endereçáveis.
As capturas de tela serão salvas na pasta \MQL5\Files\DoEasy\ScreenShots\ e os modelos serão salvos na pasta MQL5\Profiles\Templates\DoEasy\.

Para salvar arquivos de captura de tela de maneira simples, criaremos uma função no arquivo de funções de serviço \MQL5\Include\DoEasy\Services\DELib.mqh. A função retornará o nome do arquivo (que consiste no nome do programa a partir do qual é iniciada), o prefixo passado nos parâmetros da função e a hora local do computador:

//+------------------------------------------------------------------+
//| Return the file name (program name+local time)                   |
//+------------------------------------------------------------------+
string FileNameWithTimeLocal(const string time_prefix=NULL)
  {
   string name=
     (
      MQLInfoString(MQL_PROGRAM_NAME)+"_"+time_prefix+(time_prefix==NULL ? "" : "_")+
      TimeToString(TimeLocal(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)
     );
   ResetLastError();
   if(StringReplace(name," ","_")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   if(StringReplace(name,":",".")==WRONG_VALUE)
      CMessage::ToLog(DFUN,GetLastError(),true);
   return name;
  }
//+------------------------------------------------------------------+

Na função, é criada uma string a partir do nome do programa + o valor passado nos parâmetros da função + a hora local do computador no formato Data/Horas-Minutos/Segundos. Em seguida, todos os espaços são substituídos por sublinhados (_), todos os dois pontos são substituídos por pontos (.) e é retornada a string resultante. Se as substituições não funcionaram, as devidas mensagens serão exibidas no log.
Gostaria de ressaltar que a função retornará o mesmo nome de arquivo num segundo. Se chamarmos a função várias vezes num segundo, esta sempre retornará a mesma string durante esse segundo. Portanto, aqui introduzimos um parâmetro de entrada da função, podemos passar para ele informações adicionais sobre o arquivo para ser identificado de maneira exclusiva e, como complemento, para um termos conteúdo mais informativo.

Para cada janela do gráfico, podemos encontrar as coordenadas em pixels correspondentes às coordenadas de tempo/preço usando ChartXYToTimePrice(), e também fazer a conversão inversa usando ChartTimePriceToXY(). Vamos adicionar essa funcionalidade aos nossos objetos. Neste caso, a primeira função funcionará no objeto-gráfico, enquanto a segunda, no objeto da janela do gráfico. Acontece que os parâmetros da função ChartXYToTimePrice() possuem o número da subjanela em que o cursor está localizado e que é retornado por referência desde a função. Funciona em qualquer janela do gráfico, só que na variável que substituímos nos parâmetros da função, quando ela é chamada, é escrito o número da janela do gráfico onde o cursor está localizado. Mas na segunda função, para converter o tempo e o preço da janela do gráfico nas coordenadas em pixels, nós mesmos devemos passar o número da janela cujas coordenadas na forma de tempo/preço precisamos obter a partir da tela da janela. E esta é uma função, ou seja, é mais conveniente colocarmos o método que funcionará com ela no objeto da janela do gráfico, e a partir dele obteremos as coordenadas correspondentes.

Adicionamos variáveis para armazenar coordenadas do cursor na janela à seção privada da classe do arquivo \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh:

//+------------------------------------------------------------------+
//| Chart window object class                                        |
//+------------------------------------------------------------------+
class CChartWnd : public CBaseObj
  {
private:
   CArrayObj         m_list_ind;                                        // Indicator list
   int               m_window_num;                                      // Subwindow index
   int               m_wnd_coord_x;                                     // The X coordinate for the time on the chart in the window
   int               m_wnd_coord_y;                                     // The Y coordinate for the price on the chart in the window
//--- Return the flag indicating the presence of an indicator from the list in the window
   bool              IsPresentInWindow(const CWndInd *ind);
//--- Remove indicators not present in the window from the list
   void              IndicatorsDelete(void);
//--- Add new indicators to the list
   void              IndicatorsAdd(void);
//--- Set a subwindow index
   void              SetWindowNum(const int num)                        { this.m_window_num=num;   }
   
public:

Na seção pública da classe declaramos um método que converte as coordenadas do gráfico de unidades de tempo/preço em coordenadas ao longo dos eixos X e Y e escrevemos dois métodos que retornarão as coordenadas já recebidas em variáveis, bem como ummétodo que retorna a coordenada Y relativa na janela:

//--- Update data on attached indicators
   void              Refresh(void);
   
//--- Convert the coordinates of a chart from the time/price representation to the X and Y coordinates
   bool              TimePriceToXY(const datetime time,const double price);
//--- Return X and Y coordinates of the cursor location in the window
   int               XFromTimePrice(void)                         const { return this.m_wnd_coord_x;  }
   int               YFromTimePrice(void)                         const { return this.m_wnd_coord_y;  }
//--- Return the relative Y coordinate of the cursor location in the window
   int               YFromTimePriceRelative(void)  const { return this.m_wnd_coord_y-this.YDistance();}
   
  };
//+------------------------------------------------------------------+

Uma vez que os valores da coordenada Y em todas as janelas são especificados a partir da origem das coordenadas (canto superior esquerdo da janela do gráfico principal), no último método precisaremos subtrair da coordenada Y a distância até a borda superior da janela para obter a coordenada relativa à borda superior desta.

Na lista de inicialização do construtor paramétrico da classe iniciamos novas variáveis com valores padrão:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CChartWnd::CChartWnd(const long chart_id,const int wnd_num) : m_window_num(wnd_num),
                                                              m_wnd_coord_x(0),
                                                              m_wnd_coord_y(0)
  {
   CBaseObj::SetChartID(chart_id);
   this.IndicatorsListCreate();
  }
//+------------------------------------------------------------------+

Fora do corpo da classe escrevemos uma implementação do método que converte as coordenadas do gráfico de unidades de tempo/preço em coordenadas ao longo dos eixos X e Y:

//+------------------------------------------------------------------+
//| Convert chart coordinates from the time/price representation     |
//| to X and Y coordinates                                           |
//+------------------------------------------------------------------+
bool CChartWnd::TimePriceToXY(const datetime time,const double price)
  {
   ::ResetLastError();
   if(!::ChartTimePriceToXY(this.m_chart_id,this.m_window_num,time,price,this.m_wnd_coord_x,this.m_wnd_coord_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Aqui, simplesmente retornamos o resultado da função ChartTimePriceToXY(), passando todos os valores necessários para ela. Comentei a mensagem de erro no log porque recebemos muitas dessas mensagens se o cursor estiver fora do campo do gráfico e dentro do quadro da janela do gráfico.

O método grava o resultado obtido nas novas variáveis, que adicionamos acima, para armazená-las e seus valores são retornados pelos métodos XFromTimePrice() e YFromTimePrice(). Por isso, primeiro devemos chamar o método TimePriceToXY() e, depois de ele retornar true, podemos obter o valor da coordenada requerida.

Modificamos o método que atualiza os dados dos indicadores anexados à janela. Para não recriar constantemente a lista de indicadores, primeiro comparamos o número atual de indicadores na janela com o número na lista, e só se houver mudanças recriamos a lista de indicadores:

//+------------------------------------------------------------------+
//| Update data on attached indicators                               |
//+------------------------------------------------------------------+
void CChartWnd::Refresh(void)
  {
   int change=::ChartIndicatorsTotal(this.m_chart_id,this.m_window_num)-this.m_list_ind.Total();
   if(change!=0)
     {
      this.IndicatorsDelete();
      this.IndicatorsAdd();
     }
  }
//+------------------------------------------------------------------+


Modificamos a classe do objeto-gráfico localizado no arquivo \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh. No último artigo, fizemos o método WindowsTotal() funcionar de forma que, numa chamada, obtivéssemos o valor a partir do ambiente e o gravássemos nas propriedades do objeto. Porém, isso acabou não sendo muito prático a nível de código e de quantidade de chamadas para o ambiente, e resolvi abandonar essa ideia. Agora, o método simplesmente retorna o valor da propriedade do objeto:

   int WindowsTotal(void) const { return (int)this.GetProperty(CHART_PROP_WINDOWS_TOTAL); }

Vamos escrever esse valor desde o ambiente para a propriedade do objeto separadamente, onde for realmente necessário.

Adicionaremos o resto da funcionalidade adicional planejada para hoje ao objeto gráfico: navegar no gráfico, criar capturas de tela do gráfico, trabalhar com modelos do gráfico e converter as coordenadas X e Y do gráfico em valores de tempo e preço.

Anexamos o arquivo da classe CSelect ao arquivo CChartObj:

//+------------------------------------------------------------------+
//|                                                     ChartObj.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 "..\..\Objects\BaseObj.mqh"
#include "..\..\Services\Select.mqh"
#include "ChartWnd.mqh"
//+------------------------------------------------------------------+

Precisamos dele para filtrar a lista de objetos-gráficos por propriedades.

Na seção privada da classe, inserimos dois novas variáveis-membro da classe para armazenar o tempo para a coordenada X e preços para a coordenada Y no gráfico:

//+------------------------------------------------------------------+
//| Chart object class                                               |
//+------------------------------------------------------------------+
class CChartObj : public CBaseObj
  {
private:
   CArrayObj         m_list_wnd;                                  // List of chart window objects
   long              m_long_prop[CHART_PROP_INTEGER_TOTAL];       // Integer properties
   double            m_double_prop[CHART_PROP_DOUBLE_TOTAL];      // Real properties
   string            m_string_prop[CHART_PROP_STRING_TOTAL];      // String properties
   int               m_digits;                                    // Symbol's Digits()
   datetime          m_wnd_time_x;                                // Time for X coordinate on the windowed chart
   double            m_wnd_price_y;                               // Price for Y coordinate on the windowed chart
   

Aqui, na seção privada da classe, declaramos o método que adiciona uma extensão ao arquivo de captura de tela se ele estiver faltando:

//--- Create the list of chart windows
   void              CreateWindowsList(void);
//--- Add an extension to the screenshot file if it is missing
   string            FileNameWithExtention(const string filename);
   
public:

No final da listagem do corpo da classe declaramos os novos métodos que estavam planejados para hoje:

//--- Return the flag indicating that the chart object belongs to the program chart
   bool              IsMainChart(void)                               const { return(this.m_chart_id==CBaseObj::GetMainChartID());            }
//--- Return the chart window specified by index
   CChartWnd        *GetWindowByIndex(const int index)               const { return this.m_list_wnd.At(index);                               }
//--- Return the window object by its subwindow index
   CChartWnd        *GetWindowByNum(const int win_num)               const;
//--- Return the window object by the indicator name in it
   CChartWnd        *GetWindowByIndicator(const string ind_name)     const;
   
//--- Display data of all indicators of all chart windows in the journal
   void              PrintWndIndicators(void);
//--- Display the properties of all chart windows in the journal
   void              PrintWndParameters(void);

//--- Shift the chart by the specified number of bars relative to the specified chart position
   bool              Navigate(const int shift,const ENUM_CHART_POSITION position);
//--- Shift the chart (1) to the left and (2) to the right by the specified number of bars
   bool              NavigateLeft(const int shift);
   bool              NavigateRight(const int shift);
//--- Shift the chart (1) to the beginning and (2) to the end of the history data
   bool              NavigateBegin(void);
   bool              NavigateEnd(void);

//--- Create the chart screenshot
   bool              ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align);
//--- Create the screenshot of the (1) chart window, (2) 800х600 and (3) 750х562 pixels
   bool              ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   bool              ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER);
   
//--- Save the chart template with the current settings
   bool              SaveTemplate(const string filename=NULL);
//--- Apply the specified template to the chart
   bool              ApplyTemplate(const string filename=NULL);
   
//--- Convert X and Y chart window coordinates into time and price
   int               XYToTimePrice(const long x,const double y);
//--- Return (1) time and (2) price from XY coordinates
   datetime          TimeFromXY(void)                                const { return this.m_wnd_time_x;   }
   double            PriceFromXY(void)                               const { return this.m_wnd_price_y;  }
   
  };
//+------------------------------------------------------------------+

Na lista de inicialização do construtor paramétrico iniciamos novas variáveis-membros de classe com valores padrão:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CChartObj::CChartObj(const long chart_id) : m_wnd_time_x(0),m_wnd_price_y(0)
  {
  }

Vejamos a implementação de novos métodos.

Método que retorna um objeto-janela pelo nome do indicador:

//+------------------------------------------------------------------+
//| Return the window object by the indicator name in it             |
//+------------------------------------------------------------------+
CChartWnd *CChartObj::GetWindowByIndicator(const string ind_name) const
  {
   int index=(this.m_program==PROGRAM_INDICATOR && ind_name==NULL ? ::ChartWindowFind() : ::ChartWindowFind(this.m_chart_id,ind_name));
   return this.GetWindowByIndex(index);
  }
//+------------------------------------------------------------------+

Se um programa em execução baseado nesta biblioteca for um indicador, para que este indicador saiba em qual janela ele está sendo executado, precisaremos chamar a função ChartWindowFind() sem parâmetros. Se precisarmos encontrar uma janela de outro indicador, para ChartWindowFind() precisaremos passar o identificador do gráfico cujo número de janela queremos encontrar pelo nome do indicador.
Por isso, primeiro verificamos o tipo do programa, e se é um indicador e passado NULL como nome para o método, então chamamos a função ChartWindowFind() sem parâmetros, que é uma solicitação do indicador para encontrar sua própria janela.
Caso contrário, chamamos ChartWindowFind(), para o qual passamos o identificador do gráfico pertencente a este objeto-gráfico e o nome abreviado do indicador passado para o método cujo número de janela precisamos encontrar.
Como resultado, para retornar o objeto-janela onde está localizado o indicador especificado, usamos um método que retorna um objeto-janela pertencente ao objeto de gráfico dado pelo número de janela encontrado com o indicador
.

Método que desloca o gráfico o número especificado de barras em relação à posição do gráfico especificada:

//+------------------------------------------------------------------+
//| Move the chart by the specified number of bars                   |
//| relative to the specified chart position                         |
//+------------------------------------------------------------------+
bool CChartObj::Navigate(const int shift,const ENUM_CHART_POSITION position)
  {
   ::ResetLastError();
   bool res=::ChartNavigate(m_chart_id,position,shift);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

O método chama a função ChartNavigate() com parâmetros passados para o método de deslocamento - o número de barras (shift) e a posição do gráfico em relação à qual o deslocamento será realizado (position). Se a função não for bem-sucedida, o método imprimirá uma mensagem de erro no log. Será retornado o resultado da chamada da função ChartNavigate(). Antes de chamar o método, para seu correto funcionamento, é necessário desabilitar - para o objeto-gráfico - a rolagem automática para a borda direita do gráfico.

Método que desloca o gráfico para a esquerda o número especificado de barras:

//+------------------------------------------------------------------+
//| Shift the chart to the left by the specified number of bars      |
//+------------------------------------------------------------------+
bool CChartObj::NavigateLeft(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

Aqui: primeiro desligamos - para o objeto-gráfico - a rolagem automática para a borda direita do gráfico e
retornamos o resultado do método
Navegar(), para o qual passamos o valor da deslocamento do gráfico em barras, deslocamento esse transferido ao método.
Deslocamos o gráfico a partir da posição atual.

Método que desloca o gráfico para a direita o número especificado de barras:

//+------------------------------------------------------------------+
//| Shift the chart to the right by the specified number of bars     |
//+------------------------------------------------------------------+
bool CChartObj::NavigateRight(const int shift)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(-shift,CHART_CURRENT_POS);
  }
//+------------------------------------------------------------------+

Aqui: primeiro desligamos - para o objeto-gráfico - a rolagem automática para a borda direita do gráfico e
retornamos o resultado do método
Navigate(), para o qual passamos o valor negativo do deslocamento do gráfico em barras, deslocamento esse transferido ao método.
Deslocamos o gráfico a partir da posição atual.

Método que desloca o gráfico para o início dos dados históricos:

//+------------------------------------------------------------------+
//| Shift the chart to the beginning of the history data             |
//+------------------------------------------------------------------+
bool CChartObj::NavigateBegin(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_BEGIN);
  }
//+------------------------------------------------------------------+

Aqui: primeiro desligamos - para o objeto-gráfico - a rolagem automática para a borda direita do gráfico e
retornamos o resultado do método
Navigate(), para o qual transferimos o valor zero do deslocamento do gráfico em barras.
Deslocamos o gráfico para o início do histórico.

Método que desloca o gráfico para o final dos dados históricos (para a hora atual):

//+------------------------------------------------------------------+
//| Shift the chart to the end of the history data                   |
//+------------------------------------------------------------------+
bool CChartObj::NavigateEnd(void)
  {
   this.SetAutoscrollOFF();
   return this.Navigate(0,CHART_END);
  }
//+------------------------------------------------------------------+

Aqui: primeiro desligamos - para o objeto-gráfico - a rolagem automática para a borda direita do gráfico e
retornamos o resultado do método
Navigate(), para o qual transferimos o valor zero do deslocamento do gráfico em barras.
Deslocamos o gráfico para o final do histórico - para a hora atual.

Método que faz uma captura de tela do gráfico:

//+------------------------------------------------------------------+
//| Create the chart screenshot                                      |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot(const string filename,const int width,const int height,const ENUM_ALIGN_MODE align)
  {
   ::ResetLastError();
   if(!::ChartScreenShot(m_chart_id,filename,width,height,align))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

O método recebe o nome do arquivo de captura de tela, a largura e altura da imagem resultante e o alinhamento (ENUM_ALIGN_MODE). O alinhamento é necessário ao criar capturas verticais - quando a altura da imagem é maior do que sua largura. Nesse caso, o alinhamento nos informa para qual borda do gráfico a imagem será "pressionada".
Aqui, bastar tirarmos uma captura de tela com os parâmetros passados para o método usando a função ChartScreenShot().
Após a criação bem-sucedida de uma captura de tela, retornamos true.
Se não foi possível criar uma captura de tela, exibimos uma mensagem no log e retornamos false.

Como métodos adicionais para a criação de capturas de tela, teremos três métodos que criam capturas de tela de tamanhos especificados:

  1. captura de tela com o tamanho da janela do gráfico,
  2. captura de tela 800x600,
  3. captura de tela 750x562.

O segundo e o terceiro tamanhos foram escolhidos por um motivo, e é que esses tamanhos costumam ser necessários para a publicação de imagens no fórum do recurso MQL5.com, em artigos e nas descrições dos produtos do Mercado. Uma captura de tela do tamanho da janela do gráfico permitirá personalizar a aparência e o tamanho da janela diretamente no terminal e fazer uma captura de tela exatamente desse tamanho.

Método que cria uma captura de tela do gráfico usando a resolução da janela do gráfico (incluindo a escala de preço e de tempo, se houver):

//+------------------------------------------------------------------+
//| Create the chart screenshot fitting the chart window resolution  |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShotWndSize(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
//--- Create the file name or use the one passed to the method
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
//--- Get the chart window having the largest number of all windows
   CChartWnd *wnd=this.GetWindowByNum(this.m_list_wnd.Total()-1);
   if(wnd==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_ERR_FAILED_GET_WIN_OBJ),string(this.m_list_wnd.Total()-1));
      return false;
     }
//--- Calculate the screenshot width and height considering the size of the price and time scales
   int width=this.WidthInPixels()+(IsShowPriceScale() ? 56 : 0);
   int height=wnd.YDistance()+wnd.HeightInPixels()+(this.IsShowDateScale() ? 15 : 0);
//--- Create a screenshot and return the result of the ScreenShot() method
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Na listagem do método, sua lógica é comentada em detalhes. Portanto, ao criar um nome, se NULL for passado ao método, criaremos um nome de arquivo consistindo no caminho para os arquivos de captura de tela da biblioteca, no nome do programa e na extensão definida por padrão no arquivo Defines.mqh. Se o nome do arquivo não for passado vazio ao método, então usando o método FileNameWithExtention(), que consideraremos a seguir, verificamos a presença da extensão no nome do arquivo (para capturas de tela pode haver apenas três extensões: .gif, .png e .bmp) e adicionamos a extensão ao nome do arquivo se ele não existir.

Para calcular o tamanho da captura de tela levando em consideração todas as janelas pertencentes ao gráfico, precisamos encontrar a janela que tem o maior número de todas (0 - a janela do gráfico principal, 1, 2, 3, N - todas as janelas abertas de cima para baixo). A janela mais baixa terá o número mais alto. Conhecendo a distância da borda superior da janela do gráfico principal até a borda superior da janela inferior aberta no gráfico, obtemos um ponto de referência ao qual precisamos adicionar a altura dessa janela. Isso nos dará a altura total de todo o gráfico. E só temos que verificar a presença da escala de tempo no gráfico. Se houver uma escala, adicionamos 15 pixels à altura já calculada (o tamanho foi escolhido empiricamente), se não houver escala, então não adicionamos nada. É assim que encontramos a altura da futura captura de tela.

É um pouco mais fácil com a largura da captura de tela - obtemos a largura do objeto do gráfico e adicionamos 56 pixels se o gráfico tiver uma escala de preço. Se não houver escala, não adicionaremos nada. O tamanho da escala de preços também foi selecionado empiricamente.

É provável que as escalas de preço e tempo sejam diferentes em monitores diferentes com resoluções de tela diferentes - não tive chance de experimentar monitores e resoluções diferentes. No entanto, as dimensões das escalas adicionadas ao tamanho da captura de tela não causam erros notáveis de aparência da imagem - apenas a altura e a largura do arquivo é ligeiramente diferente para o gráfico de preços na janela do terminal e na captura de tela.

Método que faz uma captura de tela do gráfico em 800x600 pixels:

//+------------------------------------------------------------------+
//| Create the chart screenshot of 800x600 pixels                    |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot800x600(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=800;
   int height=600;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

Tudo é mais simples do que no método acima. O nome do arquivo é criado da mesma forma que no método anterior, e os tamanhos dos arquivos são codificados - nós os passamos para o método ScreenShot() para criar uma captura de tela e retornar o seu resultado de trabalho.

Método que faz uma captura de tela do gráfico em 750x562 pixels:

//+------------------------------------------------------------------+
//| Create the chart screenshot of 750x562 pixels                    |
//+------------------------------------------------------------------+
bool CChartObj::ScreenShot750x562(const string filename=NULL,const ENUM_ALIGN_MODE align=ALIGN_CENTER)
  {
   string name=
     (filename==NULL || filename=="" ? 
      SCREENSHOT_DIR+FileNameWithTimeLocal(this.Symbol()+"_"+TimeframeDescription(this.Timeframe()))+SCREENSHOT_FILE_EXT :  
      this.FileNameWithExtention(filename)
     );
   int width=750;
   int height=562;
   bool res=this.ScreenShot(name,width,height,align);
   if(res)
      ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_SCREENSHOT_CREATED),": ",name," (",(string)width," x ",(string)height,")");
   return res;
  }
//+------------------------------------------------------------------+

O método é semelhante ao método para criar uma captura de tela de 800x600 pixels, exceto pelas dimensões da imagem.

Qualquer gráfico com todas as suas configurações, indicadores e EAs anexados pode ser salvo como um modelo para ser aplicado posteriormente em outros gráficos. Teremos dois métodos, um para salvar o modelo do gráfico e outro para aplicar o modelo especificado ao gráfico descrito pelo objeto-gráfico.

Método que salva o modelo do gráfico com as configurações atuais:

//+------------------------------------------------------------------+
//| Save the chart template with the current settings                |
//+------------------------------------------------------------------+
bool CChartObj::SaveTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartSaveTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_SAVED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

O método recebe o nome do arquivo com o qual deve ser salvo o modelo. Se o nome estiver vazio (por padrão), serão usados o nome do programa e o nome que consiste no caminho para o arquivo definido anteriormente no arquivo Defines.mqh.
Se o arquivo do modelo for salvo com sucesso, no log será gravado um registro indicando qual modelo de gráfico foi salvo (símbolo e timeframe) e será retornado true. Se o modelo não pôde ser salvo, uma mensagem também será exibida no log e o método retornará false.

Método que aplica o modelo especificado ao gráfico:

//+------------------------------------------------------------------+
//| Apply the specified template to the chart                        |
//+------------------------------------------------------------------+
bool CChartObj::ApplyTemplate(const string filename=NULL)
  {
   ::ResetLastError();
   string name=
     (filename==NULL || filename=="" ? 
      TEMPLATE_DIR+::MQLInfoString(MQL_PROGRAM_NAME) :  
      filename
     );
   if(!::ChartApplyTemplate(this.m_chart_id,name))
     {
      CMessage::ToLog(DFUN,::GetLastError(),true);
      return false;
     }
   ::Print(DFUN,CMessage::Text(MSG_CHART_OBJ_TEMPLATE_APPLIED),": ",this.Symbol()," ",TimeframeDescription(this.Timeframe()));
   return true;
  }
//+------------------------------------------------------------------+

O método é semelhante ao método que salva o modelo de gráfico. O nome passado para o método ou o nome do arquivo de modelo gerado automaticamente também é usado. Em seguida, o modelo é aplicado ao gráfico e no log é exibido um registro sobre o resultado desta operação.

Vale a pena lembrar que se para o gráfico atual com o Expert Advisor que chamou este método aplicarmos um modelo onde está especificado outro Expert Advisor, o Expert Advisor atual será descarregado da memória e não funcionará, sendo substituído por um novo EA vindo do modelo. Este método não verifica tal colisão, portanto, sempre precisamos monitorar de forma independente os modelos e verificar a probabilidade de substituir o EA atual por aquele que pode ser iniciado a partir do modelo aplicado ao gráfico.

Método que converte as coordenadas X e Y do gráfico da janela em valores de tempo e preço:

//+------------------------------------------------------------------------------+
//|Convert X and Y coordinates of the chart window into the time and price values|
//+------------------------------------------------------------------------------+
int CChartObj::XYToTimePrice(const long x,const double y)
  {
   int sub_window=WRONG_VALUE;
   ::ResetLastError();
   if(!::ChartXYToTimePrice(this.m_chart_id,(int)x,(int)y,sub_window,this.m_wnd_time_x,this.m_wnd_price_y))
     {
      //CMessage::ToLog(DFUN,::GetLastError(),true);
      return WRONG_VALUE;
     }
   return sub_window;
  }
//+------------------------------------------------------------------+

A função ChartXYToTimePrice() converte as coordenadas X e Y do gráfico em valores de tempo e preço. Além disso, ela escreve na variável sub_window, passada a ela por referência, o número da subjanela em que estão localizadas as coordenadas X e Y do gráfico para as quais precisamos retornar a hora e o preço.
Com base nisso, este método retorna o número da subjanela do gráfico: 0 - se as coordenadas estão na janela principal do gráfico, 1,2,3, etc se as coordenadas caírem na subjanela correspondente do gráfico, e -1 se as coordenadas não puderem ser calculadas. Depois de chamar este método, e se o valor que ele retornou não for -1, podemos obter a hora e o preço usando os métodos TimeFromXY() e PriceFromXY(), que simplesmente retornam os valores das variáveis nas quais o tempo e o preço obtidos serão registrados pela função ChartXYToTimePrice()
.

Método que adiciona uma extensão ao arquivo de captura de tela, se estiver ausente:

//+------------------------------------------------------------------+
//| Add an extension to the screenshot file if it is missing         |
//+------------------------------------------------------------------+
string CChartObj::FileNameWithExtention(const string filename)
  {
   if(::StringFind(filename,FILE_EXT_GIF)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_PNG)>WRONG_VALUE || ::StringFind(filename,FILE_EXT_BMP)>WRONG_VALUE)
      return filename;
   return filename+SCREENSHOT_FILE_EXT;
  }
//+------------------------------------------------------------------+

Ao método é transferida a string no qual precisamos encontrar a extensão do arquivo de captura de tela. Uma vez que os formatos de arquivo para capturas de tela são estritamente definidos e podem ser apenas três tipos de arquivos - GIF, PNG e BMP, se a string passada para o método contiver uma ocorrência de pelo menos uma substring com esta extensão (ou seja, a extensão já está definida), então o método retorna a string passada para ele sem alterações. Caso contrário, à string é anexada a extensão do nome do arquivo definido por padrão no arquivo Defines.mqh, que é um arquivo PNG (extensão .png), e a string modificada é retornada.

Problemas identificados ao adicionar uma nova janela ao gráfico:
Durante os testes detalhados foi descoberto que no momento em que adicionamos um novo indicador ao gráfico, aparece sua janela (mas ainda não clicamos no botão "OK" ou "Cancelar"), e esta janela é imediatamente visível no terminal como já existente. No momento, a biblioteca irá vê-lo e adicionar a janela do objeto-gráfico à sua lista, e não haverá nenhum indicador nesta janela. Mas se pressionarmos cancelar na janela para adicionar um novo indicador de janela, esta janela não estará mais na lista de janelas de gráfico do terminal do cliente. A biblioteca removerá esta janela da lista na próxima verificação.

Para evitar isso e chamadas acidentais para uma janela de indicador vazia inexistente no terminal do cliente, quando uma nova janela aparecer, antes de adicioná-la à lista, vamos nos certificar de que haja um indicador nesta janela. Se houver um indicador, adicionamos uma janela; se ele não estiver, essa janela está apenas aberta, mas não adicionada ao gráfico, devemos ignorá-la.

Desta maneira, o método para criar uma lista de janelas de gráfico será melhorado:

//+------------------------------------------------------------------+
//| Create the list of chart windows                                 |
//+------------------------------------------------------------------+
void CChartObj::CreateWindowsList(void)
  {
   //--- Clear the chart window list
   this.m_list_wnd.Clear();
   //--- Get the total number of chart windows from the environment
   int total=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL);
   //--- In the loop by the total number of windows
   for(int i=0;i<total;i++)
     {
      //--- Create a new chart window object
      CChartWnd *wnd=new CChartWnd(this.m_chart_id,i);
      if(wnd==NULL)
         continue;
      //--- If the window index exceeds 0 (not the main chart window) and it still has no indicator,
      //--- remove the newly created chart window object and go to the next loop iteration
      if(wnd.WindowNum()!=0 && wnd.IndicatorsTotal()==0)
        {
         delete wnd;
         continue;
        }
      //--- If the object was not added to the list, remove that object
      this.m_list_wnd.Sort();
      if(!this.m_list_wnd.Add(wnd))
         delete wnd;
     }
   //--- If the number of objects in the list corresponds to the number of windows on the chart,
   //--- write that value to the chart object property
   //--- If the number of objects in the list does not correspond to the number of windows on the chart,
   //--- write the number of objects in the list to the chart object property.
   int value=int(this.m_list_wnd.Total()==total ? total : this.m_list_wnd.Total());
   this.SetProperty(CHART_PROP_WINDOWS_TOTAL,value);
  }
//+------------------------------------------------------------------+

A lógica do método é descrita em detalhes em sua listagem. Todas as verificações de adição desnecessária de janela ainda não criada à lista do gráfico estão localizadas no bloco marcado de código. Ao adicionar apenas um indicador de janela, podemos ter uma janela que ainda não foi criada. Portanto, não analisamos a janela com índice 0, porque ela é a janela principal do gráfico, e já existe com certeza, e só podemos adicionar novos indicadores a ela. Se encontramos uma nova janela, mas ela ainda não tem um indicador, que não pode estar com a janela do indicador já adicionada ao gráfico, então ela é a janela onde ainda não clicamos no botão "OK" para adicioná-la ao gráfico. Ignoramos tal janela para não adicioná-la à lista de janelas.

No final de todo o loop, devemos escrever o número de janelas na propriedade do objeto-gráfico. Aqui, verificamos se todas as janelas foram adicionadas à lista com sucesso - se o seu número real for igual ao seu número na lista, significa que todas as janelas foram adicionadas - escrevemos o número de janelas no gráfico na propriedade do objeto. Se esses valores não forem iguais, então na propriedade iremos escrever a quantidade que está na lista (e não é igual ao real), de modo que na próxima verificação acertemos novamente a desigualdade e adicionemos a janela já criada à lista, ou se clicamos em "Cancelar" ao adicionar uma nova janela, então o número de as janelas no gráfico e na lista serão iguais e não trataremos dessa mudança - não é o caso.

Assim fica concluída a modificação das classes da biblioteca.


Atualização automática da coleção-classe de objetos-gráficos e janelas

Agora precisamos ter certeza de que, com qualquer mudança no número de gráficos abertos, no número de janelas nos gráficos e no número de indicadores nessas janelas, a biblioteca pode atualizar automaticamente todos esses dados - para que nós mesmos não nos distraímos com isso e ao mesmo tempo ter sempre dados atualizados.

No arquivo \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh da classe do objeto do gráfico, iremos suplementar o método Refresh() para que possamos verificar não apenas a mudança no número de janelas abertas no gráfico (no objeto-gráfico), mas também controlar o número de indicadores em janelas já abertas (vários indicadores podem ser colocados numa janela).

Método que atualiza o objeto-gráfico e a suas lista de janelas:

//+------------------------------------------------------------------+
//| Update the chart object and its window list                      |
//+------------------------------------------------------------------+
void CChartObj::Refresh(void)
  {
   for(int i=0;i<m_list_wnd.Total();i++)
     {
      CChartWnd *wnd=m_list_wnd.At(i);
      if(wnd==NULL)
         continue;
      wnd.Refresh();
     }
   int change=(int)::ChartGetInteger(this.m_chart_id,CHART_WINDOWS_TOTAL)-this.WindowsTotal();
   if(change==0)
      return;
   this.CreateWindowsList();
  }
//+------------------------------------------------------------------+

Aqui: antes de verificar a alteração no número de janelas abertas no objeto-gráfico, primeiro, vamos percorrer a lista de todas as janelas do objeto e chamar o método de atualização para cada janela num loop. O método Refresh() do objeto-janela do gráfico verifica a alteração no número de indicadores colocados nele e, ao registrar as alterações, recria sua lista.

No arquivo da classe-coleção de objetos-gráficos \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh vamos consertar um erro que impede a atualização dos objetos gráficos na lista de coleções e, portando, a atualização de janelas e indicadores.

Anteriormente, o bloco para atualizar objetos-gráficos estava localizado abaixo da verificação da mudança no número de gráficos abertos, e isso não permitia chegar até ele quando não havia mudanças no número de gráficos abertos:

//+------------------------------------------------------------------+
//| Update the collection list of chart objects                      |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- Get the number of open charts in the terminal and
   int charts_total=this.ChartsTotal();
   //--- calculate the difference between the number of open charts in the terminal
   //--- and chart objects in the collection list. These values are displayed in the chart comment
   int change=charts_total-this.m_list.Total();
   Comment(DFUN,", list total=",DataTotal(),", charts total=",charts_total,", change=",change);
   //--- If there are no changes, leave
   if(change==0)
      return;
   //--- If a chart is added in the terminal
   if(change>0)
     {
      //--- Find the missing chart object, create and add it to the collection list
      this.FindAndCreateMissingChartObj();
      //--- Get the current chart and return to it since
      //--- adding a new chart switches the focus to it
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- If a chart is removed in the terminal
   else if(change<0)
    {
     //--- Find an extra chart object in the collection list and remove it from the list
     this.FindAndDeleteExcessChartObj();
    }
   //--- In the loop by the number of chart objects in the list,
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- get the next chart object and
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- update it
      chart.Refresh();
     }
  }
//+------------------------------------------------------------------+

A solução é fácil, basta mover o bloco de código de atualização de objetos-gráficos para acima - antes da verificação da mudança no número de gráficos abertos no terminal do cliente:

//+------------------------------------------------------------------+
//| Update the collection list of chart objects                      |
//+------------------------------------------------------------------+
void CChartObjCollection::Refresh(void)
  {
   //--- In the loop by the number of chart objects in the list,
   for(int i=0;i<this.m_list.Total();i++)
     {
      //--- get the next chart object and
      CChartObj *chart=this.m_list.At(i);
      if(chart==NULL)
         continue;
      //--- update it
      chart.Refresh();
     }
   //--- Get the number of open charts in the terminal and
   int charts_total=this.ChartsTotal();
   //--- calculate the difference between the number of open charts in the terminal
   //--- and chart objects in the collection list. These values are displayed in the chart comment
   int change=charts_total-this.m_list.Total();
   //--- If there are no changes, leave
   if(change==0)
      return;
   //--- If a chart is added in the terminal
   if(change>0)
     {
      //--- Find the missing chart object, create and add it to the collection list
      this.FindAndCreateMissingChartObj();
      //--- Get the current chart and return to it since
      //--- adding a new chart switches the focus to it
      CChartObj *chart=this.GetChart(GetMainChartID());
      if(chart!=NULL)
         chart.SetBringToTopON(true);
     }
   //--- If a chart is removed in the terminal
   else if(change<0)
    {
     //--- Find an extra chart object in the collection list and remove it from the list
     this.FindAndDeleteExcessChartObj();
    }
  }
//+------------------------------------------------------------------+

Agora, antes de verificar a mudança no número de gráficos abertos (e sair do método se o número não mudou), primeiro percorremos todos os objetos-gráficos na lista de coleção e verificamos a mudança de seus objetos-janelas em seu método Refresh() onde, por sua vez, seu próprio método Refresh() é chamado para verificar o número de indicadores na janela. Assim, primeiro fazemos uma verificação completa de todas as mudanças possíveis no número de indicadores em janelas, janelas de indicadores nos gráficos, e só então verificamos a mudança no número de gráficos abertos.

Vamos adicionar métodos à classe-coleção de objetos-gráficos para abrir um novo gráfico e fechar o existente.
Na seção pública da classe vamos declarar dois novos métodos:

//--- Update (1) the chart object collection list and (2) the specified chart object
   void                    Refresh(void);
   void                    Refresh(const long chart_id);

//--- (1) Open a new chart with the specified symbol and period, (2) close the specified chart
   bool                    Open(const string symbol,const ENUM_TIMEFRAMES timeframe);
   bool                    Close(const long chart_id);

  };
//+------------------------------------------------------------------+

Fora do corpo da classe, vamos escrever sua implementação.

Método que abre um novo gráfico com o símbolo e período especificados:

//+------------------------------------------------------------------+
//| Open a new chart with the specified symbol and period            |
//+------------------------------------------------------------------+
bool CChartObjCollection::Open(const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
   if(this.m_list.Total()==CHARTS_MAX)
     {
      ::Print(CMessage::Text(MSG_CHART_COLLECTION_ERR_CHARTS_MAX)," (",(string)CHARTS_MAX,")");
      return false;
     }
   ::ResetLastError();
   long chart_id=::ChartOpen(symbol,timeframe);
   if(chart_id==0)
      CMessage::ToLog(::GetLastError(),true);
   return(chart_id>0);
  }
//+------------------------------------------------------------------+

Aqui: se o número de objetos-gráficos na coleção atingiu o limite (CHARTS_MAX), então uma tentativa de abrir um novo gráfico será inútil - relatamos isso e retornamos false. Além disso, se ainda pudermos abrir um novo gráfico, chamamos a função ChartOpen() com os parâmetros especificados do gráfico aberto, e em caso de erro, reportamos no log. Retornamos o sinalizador de que a função para abrir um novo gráfico não retornou zero.

Método para fechar o gráfico especificado:

//+------------------------------------------------------------------+
//| Close a specified chart                                          |
//+------------------------------------------------------------------+
bool CChartObjCollection::Close(const long chart_id)
  {
   ::ResetLastError();
   bool res=::ChartClose(chart_id);
   if(!res)
      CMessage::ToLog(DFUN,::GetLastError(),true);
   return res;
  }
//+------------------------------------------------------------------+

Aqui: se uma tentativa de fechar o gráfico especificado por identificador não for bem sucedida - imprimimos uma entrada sobre isso no log.
Método que retorna o resultado da função ChartClose().

No arquivo \MQL5\Include\DoEasy\Engine.mqh do objeto principal da biblioteca CEngine, agregamos métodos de gerenciamento da coleção de gráficos.

Dois métodos que retornam as listas de objetos-gráficos por símbolo e timeframe

//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *ChartGetChartsList(const string symbol)                        { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *ChartGetChartsList(const ENUM_TIMEFRAMES timeframe)            { return this.m_charts.GetChartsList(timeframe);      }

renomeamos para corresponder a métodos semelhantes de outras classes e movemos um pouco mais para cima na lista:

//--- Current the chart collection
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Return (1) the chart collection and (2) the list of charts from the chart collection
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

Vamos adicionar o método que retorna o objeto-gráfico do último gráfico aberto, um método que retorna o número de objetos-gráficos na lista de coleção,
e dois métodos para abrir e fechar o gráfico especificado:

//--- Current the chart collection
   bool                 ChartCreateCollection(void)                                    { return this.m_charts.CreateCollection();            }
//--- Return (1) the chart collection and (2) the list of charts from the chart collection
   CChartObjCollection *GetChartObjCollection(void)                                    { return &this.m_charts;                              }
   CArrayObj           *GetListCharts(void)                                            { return this.m_charts.GetList();                     }
//--- Return the list of chart objects by (1) symbol and (2) timeframe
   CArrayObj           *GetListCharts(const string symbol)                             { return this.m_charts.GetChartsList(symbol);         }
   CArrayObj           *GetListCharts(const ENUM_TIMEFRAMES timeframe)                 { return this.m_charts.GetChartsList(timeframe);      }

//--- Return (1) the specified chart object, (2) the chart object with the program and (3) the chart object of the last open chart
   CChartObj           *ChartGetChartObj(const long chart_id)                          { return this.m_charts.GetChart(chart_id);            }
   CChartObj           *ChartGetMainChart(void)                                        { return this.m_charts.GetChart(this.m_charts.GetMainChartID());}
   CChartObj           *ChartGetLastOpenedChart(void)                                  { return this.m_charts.GetChart(this.GetListCharts().Total()-1);}
   
//--- Return the number of charts in the collection list
   int                  ChartsTotal(void)                                              { return this.m_charts.DataTotal();                   }

//--- Update (1) the chart specified by ID and (2) all charts
   void                 ChartRefresh(const long chart_id)                              { this.m_charts.Refresh(chart_id);                    }
   void                 ChartsRefreshAll(void)                                         { this.m_charts.Refresh();                            }

//--- (1) Open and (2) close the specified chart
   bool                 ChartOpen(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_charts.Open(symbol,timeframe);        }
   bool                 ChartClose(const long chart_id)                                { return this.m_charts.Close(chart_id);               }
   
//--- Return (1) the buffer collection and (2) the buffer list from the collection 

O método ChartGetLastOpenedChart() simplesmente retorna um ponteiro para o objeto mais recente na lista da coleção de objetos de gráfico,
e o método ChartsTotal() retorna o tamanho da lista da coleção de objetos-gráficos.
Os métodos ChartOpen() e ChartClose() retornam o resultado dos métodos Open() e Close() da classe-coleção de gráficos
.

Essas são todas as mudanças e melhorias que planejamos para hoje.


Teste

Para o teste, vamos pegar o Expert Advisor do artigo anterior e o salvamos na nova pasta \MQL5\Experts\TestDoEasy\Part70\ com o novo nome TestDoEasyPart70.mq5.

Adicionamos novos botões com os seguintes ícones ao painel do EA:

  • "<" e ">" — botão para deslocar o gráfico uma barra para a esquerda e direita, respectivamente;

  • "<<" e ">>" — botão para deslocar o gráfico dez barras para a esquerda e direita, respectivamente;

  • "|<" e ">|" — botão para definir o gráfico no início do histórico e no final do histórico, respectivamente;

  • "N" e "X" — botão para abrir um novo e para fechar o último gráfico aberto de um símbolo, respectivamente;

  • "[O]" — botão para criar uma captura de tela do gráfico atual com um Expert Advisor.

A lógica para testar a nova funcionalidade será a seguinte:

  • Se pressionarmos os botões de deslocamento do gráfico, o EA mudará o gráfico da esquerda para a direita uma barra e 10 barras, respectivamente.
  • Se pressionarmos os botões para definir o gráfico no início e no final do histórico, o gráfico ficará como devido.
  • Se pressionarmos o botão para abrir o gráfico, o EA abrirá um a um gráficos de símbolos que estão na lista da coleção de objetos-gráficos na ordem em que são encontrados na lista, e não na janela de observação do mercado (a classificação pode ser diferente na janela de observação do mercado).
    Além disso, para verificar o salvamento e a aplicação do modelo do gráfico, o EA salvará o modelo do último gráfico aberto e, ao abrir um novo, aplicará o modelo salvo nele. Ou seja, se, antes de clicar no botão para abrir um novo gráfico, abrirmos um novo gráfico manualmente, por exemplo, GBPUSD e definirmos sua aparência diferente da atual, todos os gráficos serão abertos com essa aparência. Em vez disso, um modelo com a aparência de um gráfico pré-configurado será aplicado a eles após a abertura.
    Ou seja, para este teste, primeiro precisaremos abrir um novo gráfico do símbolo GBPUSD e personalizar sua aparência. A todos os outros gráficos que já foram abertos pelo Expert Advisor será aplicado o modelo salvo a partir do gráfico configurado manualmente do símbolo GBPUSD.
  • Ao pressionar o botão para fechar o gráfico, o Expert Advisor fechará o mais recente dos gráficos abertos.
  • Ao pressionar sequencialmente o botão de captura de tela o Expert Advisor criará capturas de tela do gráfico:
    sequencialmente numa volta com esta resolução: 800x600 -> 750x562 -> Tamanho do gráfico atual.

Assim, no manipulador OnInit() do Expert Advisor vamos escrever a inclusão de permissão para rastrear eventos do mouse para o gráfico atual:

//--- Check playing a standard sound by macro substitution and a custom sound by description
   engine.PlaySoundByDescription(SND_OK);
//--- Wait for 600 milliseconds
   engine.Pause(600);
   engine.PlaySoundByDescription(TextByLanguage("Звук упавшей монетки 2","Falling coin 2"));


//--- Check the calculation of the cursor coordinates in the chart windows.
//--- Allow the current chart to track mouse movement events
   engine.ChartGetMainChart().SetEventMouseMoveON();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Isso é necessário para que o programa possa receber mensagens sobre eventos de movimento e cliques do mouse (CHARTEVENT_MOUSE_MOVE).

No manipulador OnChartEvent() quando recebemos um evento de movimento do mouse, precisamos obter as coordenadas do cursor no gráfico em pixels, convertê-los usando os métodos da biblioteca criada para os valores de tempo, preço e número da janela, em que o cursor está localizado, e, em seguida, converter a hora e o preço recém-recebidos de volta nas coordenadas do cursor dentro do gráfico e exibir todos esses valores nos comentários do gráfico.

Vamos escrever esse processamento de evento de movimento do cursor:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If working in the tester, exit
   if(MQLInfoInteger(MQL_TESTER))
      return;
//--- Handling mouse events
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Handle pressing the buttons in the panel
      if(StringFind(sparam,"BUTT_")>0)
         PressButtonEvents(sparam);
     }
//--- Handling DoEasy library events
   if(id>CHARTEVENT_CUSTOM-1)
     {
      OnDoEasyEvent(id,lparam,dparam,sparam);
     }
//--- Check ChartXYToTimePrice()
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- Get the chart object of the current (main) program chart
      CChartObj *chart=engine.ChartGetMainChart();
      if(chart==NULL)
         return;
      //--- Get the index of a subwindow the cursor is located at
      int wnd_num=chart.XYToTimePrice(lparam,dparam);
      if(wnd_num==WRONG_VALUE)
         return;
      //--- Get the calculated cursor location time and price
      datetime time=chart.TimeFromXY();
      double price=chart.PriceFromXY();
      //--- Get the window object of the chart the cursor is located in by the subwindow index
      CChartWnd *wnd=chart.GetWindowByNum(wnd_num);
      if(wnd==NULL)
         return;
      //--- If X and Y coordinates are calculated by time and price (make a reverse conversion),
      if(wnd.TimePriceToXY(time,price))
        {
         //--- in the comment, show the time, price and index of the window that are calculated by X and Y cursor coordinates,
         //--- as well as the cursor X and Y coordinates converted back from the time and price
         Comment
           (
            DFUN,"time: ",TimeToString(time),", price: ",DoubleToString(price,Digits()),
            ", win num: ",(string)wnd_num,": x: ",(string)wnd.XFromTimePrice(),
            ", y: ",(string)wnd.YFromTimePrice()," (",(string)wnd.YFromTimePriceRelative(),")")
           ;
        }
     }
  }
//+------------------------------------------------------------------+

A lógica de manipulação do evento de movimento do mouse é descrita em detalhes no código OnChartEvent() do Expert Advisor.

Na função do Expert Advisor CreateButtons() adicionamos o código para criar novos botões do painel:

//+------------------------------------------------------------------+
//| Create the buttons panel                                         |
//+------------------------------------------------------------------+
bool CreateButtons(const int shift_x=20,const int shift_y=0)
  {
   int h=18,w=82,offset=2,wpt=14;
   int cx=offset+shift_x+wpt*2+2,cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-6) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-6 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   
   h=18; offset=2;
   cx=offset+shift_x; cy=offset+shift_y+(h+1)*(TOTAL_BUTT/2)+3*h+1;
   x=cx; y=cy;
   shift=0;
   for(int i=0;i<18;i++)
     {
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name+"_PRICE",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5 : x),y,wpt,h,"P",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"P\"");
         return false;
        }
      if(!ButtonCreate(butt_data[i].name+"_TIME",((i>6 && i<14) || i>17 ? x+wpt*2+w*2+5+wpt+1 : x+wpt+1),y,wpt,h,"T",(i<4 ? clrGreen : i>6 && i<11 ? clrChocolate : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text+" \"T\"");
         return false;
        }
     }
   //--- Left and Right buttons
   int xbn=x+wpt*2+w*2+5;
   int ybn=y+h*3+3;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT1",xbn,ybn,wpt,h,"<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT1");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT1",xbn+wpt+1,ybn,wpt,h,">",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT1");
      return false;
     }
   //--- Left 10 and Right 10 buttons
   ybn=y+h*2+2;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_LEFT10",xbn,ybn,wpt,h,"<<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_LEFT10");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_RIGHT10",xbn+wpt+1,ybn,wpt,h,">>",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_RIGHT10");
      return false;
     }
   //--- Home and End buttons
   ybn=y+h+1;
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_HOME",xbn,ybn,wpt,h,"|<",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_HOME");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_NAVIGATE_END",xbn+wpt+1,ybn,wpt,h,">|",clrGreen))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_NAVIGATE_END");
      return false;
     }
   //--- Open and Close buttons
   ybn=y;
   if(!ButtonCreate(prefix+"BUTT_CHART_OPEN",xbn,ybn,wpt,h,"N",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_OPEN");
      return false;
     }
   if(!ButtonCreate(prefix+"BUTT_CHART_CLOSE",xbn+wpt+1,ybn,wpt,h,"X",clrRed))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_CLOSE");
      return false;
     }
   //--- ScreenShot button
   ybn=y-h-1;
   if(!ButtonCreate(prefix+"BUTT_CHART_SCREENSHOT",xbn,ybn,wpt*2+offset,h,"[O]",clrBlue))
     {
      Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),prefix+"BUTT_CHART_SCREENSHOT");
      return false;
     }
   
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

Aqui: para cada novo botão, são calculadas as coordenadas e é criado o botão usando a função ButtonCreate() para a qual são passados o nome do objeto gráfico criado, suas coordenadas, largura e altura, legenda e cor. Se o botão não puder ser criado, isso será relatado por um alerta e retornado false. No manipulador OnInit(), se a função de criação de painel retornou false, será realizada a saída com o código de retorno INIT_FAILED.

Na função de processamento de botões pressionados PressButtonEvents() vamos escrever o processamento de cliques em novos botões:

//+------------------------------------------------------------------+
//| Handle pressing the buttons                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   bool comp_magic=true;   // Temporary variable selecting the composite magic number with random group IDs
   string comment="";
   //--- Convert button name into its string ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- Random group 1 and 2 numbers within the range of 0 - 15
   group1=(uchar)Rand();
   group2=(uchar)Rand();
   uint magic=(comp_magic ? engine.SetCompositeMagicNumber(magic_number,group1,group2) : magic_number);
   //--- If the button is pressed
   if(ButtonState(button_name))
     {
      //--- If the button of shifting a chart 1 bar to the left is clicked
      if(button=="BUTT_NAVIGATE_LEFT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(1);
        }
      //--- If the button of shifting a chart 1 bar to the right is clicked
      if(button=="BUTT_NAVIGATE_RIGHT1")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(1);
        }
      //--- If the button of shifting a chart 10 bars to the left is clicked
      if(button=="BUTT_NAVIGATE_LEFT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateLeft(10);
        }
      //--- If the button of shifting a chart 10 bars to the right is clicked
      if(button=="BUTT_NAVIGATE_RIGHT10")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateRight(10);
        }
      //--- If the button of shifting a chart to the start of history is clicked
      if(button=="BUTT_NAVIGATE_HOME")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateBegin();
        }
      //--- If the button of shifting a chart to the end of history is clicked
      if(button=="BUTT_NAVIGATE_END")
        {
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
            chart.NavigateEnd();
        }
      //--- If the new chart open button is pressed
      if(button=="BUTT_CHART_OPEN")
        {
         int total_charts=engine.ChartsTotal();
         static int first_index=total_charts;
         string name=SymbolName(total_charts-first_index,true);
         if(engine.ChartOpen(name,PERIOD_CURRENT))
           {
            engine.ChartsRefreshAll();
            CChartObj *chart=engine.ChartGetMainChart();
            if(chart!=NULL)
               chart.SetBringToTopON(true);
           }
         //--- This code block is needed only for the test and only if there is an open GBPUSD chart
         //--- GBPUSD chart settings should differ from that of charts opened by default
         CArrayObj *list_gbpusd=engine.GetListCharts("GBPUSD");
         if(list_gbpusd!=NULL && list_gbpusd.Total()>0)
           {
            CChartObj *chart=list_gbpusd.At(0);
            if(chart.SaveTemplate())
              {
               chart=engine.ChartGetLastOpenedChart();
               if(chart!=NULL)
                  chart.ApplyTemplate();
              }
           }
         //--- End of the test code block
        }
      //--- If the the last chart close button is pressed
      if(button=="BUTT_CHART_CLOSE")
        {
         CArrayObj *list_charts=engine.GetListCharts();
         if(list_charts!=NULL)
           {
            list_charts.Sort(SORT_BY_CHART_ID);
            CChartObj *chart=list_charts.At(list_charts.Total()-1);
            if(chart!=NULL && !chart.IsMainChart())
              engine.ChartClose(chart.ID());
           }
        }
      //--- If the ScreenShot button is pressed
      if(button=="BUTT_CHART_SCREENSHOT")
        {
         static int num=0;
         if(++num>3) num=1;
         CChartObj *chart=engine.ChartGetMainChart();
         if(chart!=NULL)
           {
            switch(num)
              {
               case 1 : chart.ScreenShot800x600(); break;
               case 2 : chart.ScreenShot750x562(); break;
               default: chart.ScreenShotWndSize(); break;
              }
           }
        }
      
      //--- If the 'BUTT_BUY: Open Buy position' is pressed
      if(button==EnumToString(BUTT_BUY))
        {
         ...
         ...
         ...
       ...
       ...
     ...
     ...

O código da função não é mostrado por completo, mas apenas as alterações adicionadas.
Aqui, lidamos apenas com cada novo pressionamento de botão. A lógica é simples e não vou descrevê-la - vou deixá-la para uma análise independente, especialmente porque esse manipulador serve apenas como um exemplo de como você pode trabalhar com métodos de biblioteca. Você sempre pode tirar todas as suas dúvidas sobre o código nos comentários do artigo.

Essas são todas as melhorias que precisávamos fazer no novo EA de teste.

Vamos compilar o Expert Advisor e executá-lo num gráfico de qualquer símbolo com as configurações "Usar o símbolo atual" e "Usar o período gráfico atual":


Antes de iniciar o Expert Advisor, certifique-se de abrir um novo gráfico do símbolo GBPUSD e configurar sua aparência de forma diferente dos gráficos abertos por padrão ao usar o modelo default.tpl, por exemplo, como este (o gráfico GBPUSD foi aberto antecipadamente) :



Agora podemos testar a nova funcionalidade da biblioteca pressionando os botões do painel:


Cada vez que abrimos um novo gráfico, o EA salva o modelo do símbolo GBPUSD, que configuramos previamente, o aplica a cada gráfico aberto recentemente e imprime uma mensagem sobre isso no log:

CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDCHF H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: GBPUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURUSD H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: USDRUB H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURJPY H1
CChartObj::SaveTemplate: Chart template saved: GBPUSD H4
CChartObj::ApplyTemplate: Template applied to the chart: EURGBP H1
CChartObjCollection::Close: Wrong chart ID (4101)

Ao fechar gráficos abertos houve um erro. A biblioteca atualiza o estado dos gráficos abertos a cada meio segundo.
Isso é definido no arquivo Defines.mqh:

//--- Parameters of the chart collection timer
#define COLLECTION_CHARTS_PAUSE        (500)                      // Chart collection timer pause in milliseconds
#define COLLECTION_CHARTS_COUNTER_STEP (16)                       // Chart timer counter increment
#define COLLECTION_CHARTS_COUNTER_ID   (9)                        // Chart timer counter ID

Acontece que apertei o botão fechar do último gráfico aberto mais rápido que duas vezes por segundo, e houve uma tentativa de fechar o gráfico anterior já fechado, e na lista da coleção de gráficos ficou um registro disso. A frequência de atualização do estado atual dos gráficos abertos, suas janelas e indicadores podem ser ajustados alterando a substituição de macro especificada. Para atualizar o ambiente mais rapidamente, precisamos diminuir o valor dessa constante. Nesse caso, a carga do processador aumentará devido ao aumento da frequência de atualizações. Aqui precisamos encontrar o "meio-termo", uma vez que esta funcionalidade ainda se destina ao controle manual de gráficos, de modo que a frequência das atualizações é individual e, às vezes, os erros no acesso a gráficos ausentes não são críticos - basta pressionar o botão uma segunda vez quando ocorrer a próxima atualização do ambiente e a sincronização da lista de objetos do gráfico na biblioteca com seu estado no terminal do cliente.

Resta testar a criação de capturas de tela do gráfico atual. Cada vez que o botão é pressionado, uma captura de tela do gráfico é criada com um determinado tamanho. Primeiro pressionamento - captura de tela 800x600, segundo pressionamento - captura de tela 750x562, terceiro pressionamento - captura de tela com o tamanho do gráfico atual:


Depois de criar três capturas de tela com diferentes resoluções, com as devidas mensagens no log,

CChartObj::ScreenShot800x600: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.25.png (800 x 600)
CChartObj::ScreenShot750x562: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.28.png (750 x 562)
CChartObj::ScreenShotWndSize: Screenshot created: DoEasy\ScreenShots\TestDoEasyPart70_EURUSD_H1_2021.04.13_14.02.29.png (726 x 321)

Também examinamos o conteúdo da pasta na qual essas capturas de tela são salvas.
Antes de fazer as capturas de tela, movi o cursor em lugares diferentes das duas janelas do gráfico atual, e nos comentários do gráfico podemos ver a hora, preço, número da subjanela e as coordenadas X e Y do cursor em pixels. A coordenada Y do cursor tem dois valores. O primeiro valor exibe a coordenada Y relativa às coordenadas iniciais da janela do símbolo principal, o segundo valor exibido (entre colchetes) mostra o valor da coordenada Y relativa à borda superior da janela onde o cursor está localizado.

Como podemos ver, todas as funcionalidades planejadas para hoje estão funcionando corretamente.


Teste O que vem agora?

No próximo artigo, geraremos o rastreamento automático dos eventos de alteração das propriedades dos objetos-gráficos e de suas janelas.

Todos os arquivos da versão atual da biblioteca e o arquivo do EA de teste para MQL5 estão anexados abaixo. Você pode baixá-los e testar tudo sozinho.
Se você tiver perguntas, comentários e sugestões, poderá expressá-los nos comentários do artigo.

Complementos

*Artigos desta série:

Outras classes na biblioteca DoEasy (Parte 68): classe de objeto-gráfico e classes de objetos-indicadores na janela do gráfico
Outras classes na biblioteca DoEasy (Parte 69): classe-coleção de objetos-gráficos

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/9293

Arquivos anexados |
MQL5.zip (3951.46 KB)
Outras classes na biblioteca DoEasy (Parte 71): eventos da coleção de objetos-gráficos Outras classes na biblioteca DoEasy (Parte 71): eventos da coleção de objetos-gráficos
Neste artigo, criaremos uma funcionalidade para rastrear alguns eventos de objetos-gráficos - adição/remoção de gráficos de símbolos, de subjanelas do gráfico, bem como adição/exclusão/mudança de indicadores presentes em janelas de gráficos.
Swaps (Parte I): bloqueio e posições sintéticas Swaps (Parte I): bloqueio e posições sintéticas
Neste artigo, tentarei expandir o conceito clássico de métodos de negociação de swap, e também explicarei porque cheguei à conclusão de que ele, em minha opinião, merece atenção especial e vale absolutamente a pena ser estudado.
Conselhos de um programador profissional (Parte II): armazenamento e troca de parâmetros entre um EA, scripts e programas externos Conselhos de um programador profissional (Parte II): armazenamento e troca de parâmetros entre um EA, scripts e programas externos
Conselhos de um programador profissional sobre métodos, técnicas e ferramentas auxiliares para tornar a programação mais fácil. Hoje falaremos sobre os parâmetros que podem ser restaurados após reiniciar (fechar) o terminal. Na verdade, todos os exemplos apresentados são partes funcionais do código do meu projeto Cayman.
Conselhos de um programador profissional (Parte I): Armazenamento, depuração e compilação de códigos Trabalho com projetos e registros Conselhos de um programador profissional (Parte I): Armazenamento, depuração e compilação de códigos Trabalho com projetos e registros
Conselhos de um programador profissional sobre métodos, técnicas e ferramentas auxiliares para tornar a programação mais fácil.