Otras clases en la biblioteca DoEasy (Parte 70): Ampliación de la funcionalidad y actualización automática de la colección de objetos de gráfico

Artyom Trishkin | 29 junio, 2021

Contenido


Concepto

En el artículo anterior, creamos una colección de objetos de gráfico. Ahora, cada gráfico de símbolos abierto en el terminal está representado por un objeto de gráfico. En su composición, cada objeto de gráfico tiene un conjunto de objetos de ventana en el que se ubican los objetos de indicador de la ventana. Cualquier objeto de gráfico tiene al menos un objeto de ventana: nos referimos a la ventana principal del gráfico. Todas las demás ventanas de los indicadores se pueden añadir y eliminar de la lista de ventanas de los gráficos. Hemos ubicado todo este conjunto de objetos en la colección de objetos de gráfico.

Las pruebas detalladas de la colección de objetos de gráfico del artículo anterior han revelado algunos problemas al añadir nuevas ventanas a la ventana principal del gráfico. Hoy arreglaremos dichos problemas. Además, añadiremos nuevas funcionalidades a los objetos de gráfico: la navegación en la ventana del gráfico de un símbolo, la creación de capturas de pantalla de las ventanas y el almacenamiento y la carga de plantillas en el gráfico.

Además de las mejoras planificadas, hoy implementaremos el monitoreo automático de algunos eventos que suceden con los gráficos en el terminal de cliente y con las ventanas de los objetos de gráfico: la adición/eliminación de un gráfico existente de un símbolo (objeto de gráfico), la adición/eliminación de una ventana existente de un indicador a un objeto de gráfico, y la adición de un indicador nuevo/eliminación de un indicador existente de la ventana del gráfico.


Mejorando las clases de la biblioteca

Procedamos. En primer lugar (como hacemos normalmente), en el archivo \MQL5\Include\DoEasy\Data.mqh, añadimos los índices de los nuevos mensajes:

   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
  
  };
//+------------------------------------------------------------------+

y los textos de los mensajes que se corresponden con los índices nuevamente añadidos:

   {"Главное окно графика","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 hoy vamos a crear una funcionalidad adicional de los objetos de gráfico que incluirá la creación de capturas de pantalla y el trabajo con plantillas, necesitaremos especificar las carpetas de guardado para las capturas de pantalla y las plantillas, así como la extensión del nombre del archivo por defecto (y, por lo tanto, el formato de archivo de la imagen guardada) para las capturas de pantalla. Los tipos de archivo disponibles para guardar las capturas de pantalla pueden ser *.gif, *.png y *.bmp.

En el archivo \MQL5\Include\DoEasy\Defines.mqh, añadimos estas nuevas macrosustituciones:

//--- 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

Debemos señalar que las carpetas de guardado para las capturas de pantalla y las plantillas en el terminal son distitntas.

Las capturas de pantalla se guardan en la carpeta (directorio de datos del terminal)\MQL5\Files\

Y las plantillas se guardan en la carpeta (directorio de datos del terminal)\ MQL5\Profiles\Templates\

Por ello, la adición de las sustituciones de las macros especificadas al nombre del archivo hará que el almacenamiento de los archivos de la biblioteca resulte más accesible.
Las capturas de pantalla se guardarán en la carpeta \MQL5\Files\DoEasy\ScreenShots\ y las plantillas se guardarán en la carpeta MQL5\Profiles\Templates\DoEasy\.

Para que guardar los archivos de captura de pantalla resulte más cómodo, crearemos una función en el archivo de funciones de servicio \MQL5\Include\DoEasy\Services\DELib.mqh. La función retornará un nombre de archivo que constará del nombre del programa desde el cual se ha iniciado, el prefijo transmitido en los parámetros de la función y la hora de la computadora local:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

La función crea una línea a partir del nombre del programa + el valor transmitido en los parámetros de la función + la hora de la computadora local en el formato Fecha/Horas-Minutos/Segundos. Luego, todos los espacios son reemplazados con guiones bajos (_) y todos los dos puntos son reemplazados con puntos (.); después se retorna la línea resultante. Si los reemplazos no han funcionado, se mostrarán los mensajes correspondientes sobre ello en el diario.
Cabe señalar que la función retornará el mismo nombre de archivo en el plazo de un segundo, es decir, si llamamos a la función varias veces en un segundo, siempre retornará la misma línea durante este segundo. Por lo consiguiente, aquí introduciremos un parámetro de entrada para la función en el que podremos transmitir información adicional sobre el archivo para que su identificación resulte única y, como complemento, para que el contenido resulte más informativo.

Para cada ventana del gráfico, podemos encontrar las coordenadas en píxeles correspondientes a las coordenadas de tiempo/precio utilizando ChartXYToTimePrice(), y también hacer la transformación inversa usando ChartTimePriceToXY(). Vamos a añadir esta funcionalidad a nuestros objetos. En este caso, la primera función trabajará en el objeto de gráfico, mientras que la segunda trabajará en el objeto de ventana del gráfico. El caso es que la función ChartXYToTimePrice() tiene en sus parámetros el número de la subventana en la que se encuentra el cursor, y que se retorna por enlace desde la función. Funciona en cualquier ventana de gráfico; solo registra en la variable que sustituimos en los parámetros de la función (al llamar a esta) el número de la ventana del gráfico en la que se encuentra el cursor. Pero en la segunda función, para convertir la hora y el precio en la ventana del gráfico a las coordenadas de pantalla correspondientes en píxeles, deberemos transmitir nosotros mismos el número de esa ventana, cuyas coordenadas en forma de tiempo/precio necesitamos obtener de las coordenadas de la pantalla de la ventana. Y esta función, es decir, el método que va a trabajar con ella, resulta más conveniente colocarlo en el objeto de la ventana del gráfico, y obtener de él las coordenadas correspondientes.

En el archivo \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh, en la sección privada de la clase, añadimos las variables para guardar las coordenadas del cursor en la ventana:

//+------------------------------------------------------------------+
//| 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:

En la sección pública de la clase, declaramos el método que convierte las coordenadas del gráfico de la representación de tiempo/precio en coordenadas a lo largo de los ejes X e Y y escribimos dos métodos que retornan las coordenadas ya recibidas a las variables, así como un método que retorna la coordenada Y respecto a la ventana:

//--- 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();}
   
  };
//+------------------------------------------------------------------+

Como los valores de la coordenada Y en todas las ventanas se indican a partir del origen de las coordenadas (la esquina superior izquierda de la ventana principal del gráfico), para obtener la coordenada relativa al borde superior de la ventana, necesitaremos restar la distancia al borde superior de esta ventana de la coordenada Y, que es lo que haremos en el último método.

En la lista de inicialización del constructor paramétrico de la clase, inicializamos las nuevas variables con los valores predeterminados:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Fuera del cuerpo de la clase, escribimos la implementación del método que convierte las coordenadas del gráfico de la representación del tiempo/precio a las coordenadas a lo largo de los ejes 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;
  }
//+------------------------------------------------------------------+

Aquí, solo tenemos que retornar el resultado de la función ChartTimePriceToXY(), transmitiendo en ella todos los valores necesarios. Hemos comentado el mensaje de error en el diario porque recibimos demasiados mensajes así en el mismo si el cursor se encuentra fuera del campo del gráfico, pero dentro del marco de la ventana del gráfico.

El método escribe el resultado obtenido en las variables recién agregadas para almacenarlas, mientras que los métodos XFromTimePrice() y YFromTimePrice() retornan los valores de las variables. Por consiguiente, primero debemos llamar al método TimePriceToXY(), y después de que retorne true, podremos obtener el valor de la coordenada que necesitamos.

Vamos a mejorar el método para actualizar los datos de los indicadores fijados a la ventana. Para no volver a crear constantemente la lista de indicadores, primero comparamos el número actual de indicadores en la ventana con el número en la lista, y solo si hay cambios crearemos nuevamente la 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();
     }
  }
//+------------------------------------------------------------------+


Vamos a mejorar la clase del objeto gráfico ubicada en el archivo \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh. En el último artículo, hicimos que el método WindowsTotal() funcionara de tal forma que en una llamada podamos obtener directamente el valor del entorno y escribirlo en las propiedades del objeto. No obstante, esto no resultaba muy práctico desde el punto de vista de la claridad de la construcción de la lógica del código y el número de llamadas al entorno, y decidimos abandonar esta idea. Ahora, el método simplemente retorna el valor de la propiedad del objeto:

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

Debemos establecer el valor del entorno en la propiedad de objeto donde sea realmente necesario.

Vamos a añadir al objeto gráfico el resto de la funcionalidad adicional planificada para hoy: la navegación por el gráfico, la creación de capturas de pantalla del gráfico, el trabajo con las plantillas de los gráficos y la conversión de las coordenadas X e Y del gráfico en valores de tiempo y precio.

Añadimos al archivo de clase CChartObj el archivo de clase CSelect:

//+------------------------------------------------------------------+
//|                                                     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"
//+------------------------------------------------------------------+

Lo necesitaremos para filtrar la lista de objetos de gráfico según sus propiedades.

En la sección privada de la clase, escribiremos las dos nuevas variables de miembro de clase para almacenar el tiempo de la coordenada X y el precio de la coordenada Y en el 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
   

Aquí mismo, en la sección privada de la clase, declaramos el método que añade una extensión al archivo de captura de pantalla si dicha extensión falta:

//--- 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:

Al final de la lista del cuerpo de la clase , declaramos los nuevos métodos que planeábamos para hoy:

//--- 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;  }
   
  };
//+------------------------------------------------------------------+

En la lista de inicialización del constructor paramétrico, inicializamos las nuevas variables de miembro de clase con los valores predeterminados:

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

Vamos a analizar la implementación de los nuevos métodos.

Método que retorna un objeto de ventana según el nombre del indicador que contiene:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Si el programa en ejecución basado en esta biblioteca es un indicador, para que este indicador sepa en qué ventana se inicia, deberemos llamar a la función ChartWindowFind() sin parámetros. Si necesitamos encontrar la ventana de otro indicador, deberemos transmitir a ChartWindowFind() el identificador del gráfico cuyo número de ventana queremos encontrar según el nombre del indicador.
Por consiguiente, aquí primero verificamos el tipo de programa, y ​​si se trata de un indicador y al método se transmite NULL como nombre, llamaremos a la función ChartWindowFind() sin parámetros: se trata de una solicitud del indicador para buscar en su propia ventana.
De lo contrario, llamaremos a ChartWindowFind(), al que transmitiremos el identificador de gráfico que pertenece a este objeto de gráfico y el nombre breve del indicador transmitido al método cuyo número de ventana necesitamos encontrar.
Como resultado, para retornar el objeto de ventana en el que se encuentra el indicador indicado, usaremos el método que retorna el objeto de ventana perteneciente a este objeto gráfico según el número de ventana encontrado con el indicador.

Método que desplaza el gráfico según el número indicado de barras respecto a la posición del gráfico especificado:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

El método simplemente llama a la función ChartNavigate() con los parámetros del cambio transmitidos ​​al método: el número de barras (shift) y  la posición del gráfico respecto a la que se realizará el cambio (position). Si la función falla, el método imprimirá un mensaje de error en el diario. Se retorna el resultado de la llamada a la función ChartNavigate(). Antes de llamar al método, para que este funcione correctamente, será necesario deshabilitar el desplazamiento automático hacia el borde derecho del gráfico para el objeto gráfico.

Método que desplaza el gráfico hacia la izquierda el número de barras indicado:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

Aquí, primero desactivamos para el objeto de gráfico el desplazamiento automático hacia el borde derecho del gráfico y
retornamos el resultado del funcionamiento del método
Navigate(), al que transmitimos el valor del desplazamiento del gráfico en barras transmitido al método.
Desplazamos el gráfico respecto a la posición actual.

Método que desplaza el gráfico hacia la derecha el número indicado 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);
  }
//+------------------------------------------------------------------+

Aquí, primero desactivamos para el objeto de gráfico el desplazamiento automático hacia el borde derecho del gráfico y
retornamos el resultado del funcionamiento del método
Navigate(), al que transmitimos el valor negativo del desplazamiento del gráfico en barras transmitido al método.
Desplazamos el gráfico respecto a la posición actual.

Método que desplaza el gráfico hacia el comienzo de los datos históricos:

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

Aquí, primero desactivamos para el objeto de gráfico el desplazamiento automático hacia el borde derecho del gráfico y
retornamos el resultado del funcionamiento del método
Navigate(), al cual transmitimos el valor cero del desplazamiento del gráfico en barras.
Desplazamos el gráfico hacia el comienzo de la historia.

Método para desplazar el gráfico hacia el final de los datos históricos (hacia el momento actual):

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

Aquí, primero desactivamos para el objeto de gráfico el desplazamiento automático hacia el borde derecho del gráfico y
retornamos el resultado del funcionamiento del método
Navigate(), al cual transmitimos el valor cero del desplazamiento del gráfico en barras.
Desplazamos el gráfico hacia el final de la historia, es decir, hacia el momento actual.

Método que crea una captura de pantalla del 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;
  }
//+------------------------------------------------------------------+

Transmitimos al método el nombre del archivo de la captura de pantalla, el ancho y el alto de la imagen resultante y la alineación (ENUM_ALIGN_MODE). La alineación es necesaria al crear tomas verticales, cuando la altura de la imagen es mayor que su anchura. En este caso, la alineación nos dirá a qué borde del gráfico se "apretará" la imagen.
Aquí solo tomamos una captura de pantalla con los parámetros transmitidos ​​al método utilizando la función ChartScreenShot().
Si se crea una captura de pantalla exitosa, retornaremos true.
Si no ha sido posible crear una captura de pantalla, mostraremos un mensaje sobre ello en el diario y retornaremos false.

Como métodos adicionales para crear capturas de pantalla, tendremos tres métodos que crean capturas de pantalla de tamaños específicos:

  1. captura de pantalla según el tamaño de la ventana del gráfico,
  2. captura de pantalla 800x600,
  3. captura de pantalla 750x562.

Hemos elegido el segundo y tercer tamaño por un motivo: estos tamaños a menudo se requieren para la publicación de imágenes en el foro de MQL5.com, en los artículos y en las descripciones de los productos del mercado La captura de pantalla según el tamaño de la ventana del gráfico nos permitirá personalizar el aspecto y el tamaño de la ventana directamente en el terminal y hacer una captura de pantalla exactamente de este tamaño.

Método que crea una captura de pantalla del gráfico en la resolución de la ventana del gráfico (incluyendo la escala de precio y las escalas temporales, si las hubiera):

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

En el listado del método, comentamos su lógica con detalle. Resumiendo: al crear un nombre, si transmitimos al método NULL, crearemos un nombre de archivo que constará de la ruta a los archivos de captura de pantalla de la biblioteca, el nombre del programa y la extensión establecida por defecto en el archivo Defines.mqh. Si el nombre del archivo transmitido al método no está vacío, entonces, utilizando el método FileNameWithExtention() que analizaremos a continuación, verificaremos la presencia de la extensión en el nombre del archivo (para capturas de pantalla solo puede haber tres extensiones: .gif, .png y .bmp) y añadiremos la extensión al nombre del archivo si falta.

Para calcular el tamaño de la captura de pantalla considerando todas las ventanas pertenecientes al gráfico, necesitaremos encontrar la ventana con el mayor número de todas (0 - ventana principal del gráfico, 1, 2, 3, N — todas las ventanas abiertas en él lo están de arriba hacia abajo). es decir, la ventana más baja tendrá el número más alto. Conociendo la distancia desde el borde superior de la ventana principal del gráfico hasta el borde superior de la ventana inferior abierta en el gráfico, obtendremos el punto de referencia al que debemos añadir la altura de esta ventana. Esto nos dará la altura completa de todo el gráfico. A partir de ahí, solo tendremos que comprobar la presencia de la escala de tiempo en el gráfico. Si hay una escala, añadiremos 15 píxeles a la altura ya calculada (el tamaño se ha seleccionado empíricamente), si no hay una escala, entonces no añadiremos nada. Así es como encontraremos la altura de la futura captura de pantalla.

La anchura de la captura de pantalla es algo más fácil de averiguar: obtenemos la anchura del objeto del gráfico y le añadimos 56 píxeles si el gráfico tiene una escala de precios. Si no hubiera escala, no añadimos nada. El tamaño de la escala de precios también se ha seleccionado empíricamente.

Es probable que los precios y las escalas de tiempo resulten distintos en diferentes monitores con distintas resoluciones de pantalla; no hemos tenido la oportunidad de experimentar con diferentes monitores y sus resoluciones. No obstante, las dimensiones de las escalas añadidas al tamaño de la captura de pantalla no introducen un gran error en el aspecto de la imagen: solo el propio tamaño del archivo según su altura y anchura, y ligeramente distinto en cuanto a la apariencia del gráfico de precios en la ventana del terminal en la captura de pantalla.

Método que crea una captura de pantalla del gráfico con una resolución de 800x600 píxeles:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Aquí, todo es más sencillo que en el método anterior. El nombre del archivo se crea de la misma forma que en el método anterior, mientras que los tamaños del archivo aquí se establcen de forma estricta; los transmitimos al método ScreenShot() para crear una captura de pantalla y retornar el resultado de su funcionamiento.

Método que crea una captura de pantalla del gráfico con una resolución de 750x562 píxeles:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

El método es similar al de 800x600 píxeles, salvo por las dimensiones de la imagen.

Podemos guardar como plantilla cualquier gráfico con todos sus indicadores, configuraciones y asesores adjuntos, para aplicar posteriormente esta a otros gráficos. Tendremos dos métodos: para guardar la plantilla del gráfico y para aplicar la plantilla indicada al gráfico descrito por el objeto de gráfico.

Método que guarda la plantilla de gráfico con la configuración actual:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Transmitimos al método el nombre del archivo en el que se debe guardar la plantilla. Si el nombre está vacío (por defecto), entonces usaremos un nombre que conste de la ruta al archivo definido anteriormente en el archivo Defines.mqh y el nombre del programa.
Si el archivo de plantilla se ha guardado correctamente, en el diario se mostrará una entrada que indicará qué plantilla de gráfico se ha guardado (símbolo y marco temporal) y se retornará true. Si no se ha podido guardar la plantilla, también se mostrará un mensaje sobre ello en el diario y el método retonará false.

Método que aplica la plantilla especificada al 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;
  }
//+------------------------------------------------------------------+

El método es similar al método que guarda la plantilla de gráfico. También se usa el nombre del archivo de plantilla transmitido al método o generado automáticamente. Luego, la plantilla se aplica al gráfico y se muestra una entrada en el diario sobre el resultado de esta operación.

Debemos tener en cuenta que si aplicamos una plantilla con otro asesor al gráfico actual con un asesor en ejecución que ha llamado al método, el asesor actual se descargará de la memoria y ya no funcionará: será reemplazado con el nuevo asesor de la plantilla. Este método no comprueba la posibilidad de tal colisión, por lo que siempre deberemos monitorear de forma independiente nuestra plantillas y verificar la probabilidad de que el asesor actual sea reemplazado por el que se pueda iniciar desde la plantilla aplicada al gráfico.

Método que convierte las coordenadas X e Y del gráfico de la ventana en valores de tiempo y precio:

//+------------------------------------------------------------------------------+
//|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;
  }
//+------------------------------------------------------------------+

La función ChartXYToTimePrice() convierte las coordenadas X e Y del gráfico en valores de tiempo y precio. Al mismo tiempo, escribe en la variable sub_window transmitida por enlace el número de la subventana en la que se ubican las coordenadas X e Y del gráfico, para lo cual será necesario retornar la hora y el precio.
Usando esto como base, el método retorna el número de la subventana del gráfico: 0, si las coordenadas están en la ventana principal del gráfico, 1,2,3, etcétera, si las coordenadas caen en la subventana correspondiente del gráfico, y -1, si no se han podido calcular las coordenadas. Después de llamar a este método, y si el valor retornado no es -1, podemos obtener la hora y el precio usando los métodos TimeFromXY() y PriceFromXY(), que simplemente retornan los valores de las variables en las que escribirá la hora y el precio con la función ChartXYToTimePrice().

Método que añade una extensión al archivo de captura de pantalla si esta falta:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

La línea de código marcada es transmitida al método en el que debemos encontrar la extensión del archivo de captura de pantalla. Dado que los formatos de archivo para las capturas de pantalla están estrictamente definidos y solo pueden ser tres tipos de archivos: GIF, PNG y BMP, entonces si la línea transmitida al método contiene al menos una subcadena con esta extensión (es decir, la extensión ya está configurada), el método retornará luego la cadena que se le ha transmitido sin cambios. De lo contrario, la extensión del nombre del archivo se añadirá a la línea establecida por defecto en el archivo Defines.mqh, y este es un archivo PNG (extensión .png), y la línea modificada es retornada.

Acerca de los problemas identificados al añadir una nueva ventana al gráfico:
Las pruebas detalladas han revelado que en el momento en que añadimos un nuevo indicador a la ventana del gráfico, aparece su ventana (pero aún no hemos clicado en el botón "Aceptar" o "Cancelar"), y esta ventana se hace inmediatamente visible en el terminal como ya existente. Por el momento, la biblioteca lo verá y añadirá la ventana del objeto de gráfico a su lista, mientras que en esta ventana no habrá ningún indicador. Pero si pulsamos "cancelar" en la ventana de adición de un nuevo indicador de ventana, dicha ventana ya no se encontrará en la lista de ventanas del gráfico del terminal de cliente. La biblioteca eliminará esta ventana de la lista en la próxima verificación.

Para evitar acciones innecesarias y llamadas accidentales a una ventana de indicador vacía que no existe en el terminal de cliente, cuando aparezca una nueva ventana, antes de añadir esta a la lista, deberemos asegurarnos de que haya un indicador en dicha ventana. Si hay un indicador, añadiremos la ventana; si no se encuentra allí, dicha ventana solo estará abierta, pero no se añadirá al gráfico: deberemos omitirla.

Por consiguiente, vamos a mejorar el método encargado de crear la lista de ventanas de gráfico:

//+------------------------------------------------------------------+
//| 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);
  }
//+------------------------------------------------------------------+

La lógica del método se describe con detalle en su listado. La verificación de la necesidad de añadir o no a la lista una ventana que aún no se ha creado se encuentra en el bloque de código resaltado. Es decir, al añadir solo un indicador de ventana, es posible que tengamos una ventana que aún no se ha creado. Por consiguiente, no analizamos la ventana con el índice 0; esta es la ventana principal del gráfico, y seguro que ya existe: solo podemos añadirle nuevos indicadores. Si hemos encontrado una nueva ventana, pero en ella aún no hay ningún indicador, cosa que no puede suceder con la ventana del indicador ya añadida al gráfico, se tratará de una ventana en la que aún no hemos clicado sobre el botón "Aceptar" para su adición al gráfico. Omitiremos dicha ventana para no añadirla a la lista de ventanas.

Al final de todo el ciclo, deberemos escribir en la propiedad del objeto de gráfico su cantidad de ventanas. Aquí comprobaremos que todas las ventanas se hayan añadido con éxito a la lista; si su número real es igual a su número en la lista, significará que se han añadido todas las ventanas: por consiguiente, escribiremos en las propiedades del objeto el número de ventanas en el gráfico. Si los valores no son iguales, escribiremos en la propiedad la cantidad que se encuentra en la lista (y que no es igual a la real), para que en la siguiente verificación no alcancemos de nuevo la desigualdad y podamos añadir a la lista la ventana finalmente creada; o bien, al añadir una nueva ventana, podemos clicar en "Cancelar": el número de ventanas en el gráfico y en la lista será igual, y no procesaremos el cambio, porque no lo hay.

Ya hemos terminado las mejoras de las clases de la biblioteca.


Actualización automática de la clase de colección de objetos de los gráficos y ventanas

Ahora debemos asegurarnos de que con cualquier cambio en el número de gráficos abiertos, en el número de ventanas en los gráficos y en el número de indicadores en estas ventanas, la biblioteca pueda actualizar automáticamente todos estos datos, para que nosotros mismos no tengamos que distraernos con esto y al mismo tiempo podamos tener siempre los datos actualizados.

En el archivo \MQL5\Include\DoEasy\Objects\Chart\ChartObj.mqh de la clase del objeto gráfico, añadimos el método Refresh() para que podamos comprobar no solo el cambio en el número de ventanas abiertas en el gráfico (en el objeto de gráfico), sino también controlar el número de indicadores en las ventanas ya abiertas (se pueden colocar varios indicadores en una ventana).

Método que actualiza el objeto gráfico y la lista de sus ventanas:

//+------------------------------------------------------------------+
//| 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();
  }
//+------------------------------------------------------------------+

Aquí, antes de verificar el cambio en el número de ventanas abiertas en el objeto de gráfico, primero revisamos la lista con todas las ventanas del objeto y llamamos al método de actualización para cada ventana subsiguiente en el ciclo. El método Refresh() del objeto de ventana del gráfico comprueba el cambio en el número de indicadores colocados en él y, al registrar los cambios, vuelve a crear su lista.

En el archivo de la clase de colección de objetos de gráfico \MQL5\Include\DoEasy\Collections\ChartObjCollection.mqh, solucionamos el error lógico que impide actualizar los objetos de gráfico en la lista de colecciones y, en consecuencia , la actualización de sus ventanas y los indicadores en ellas.

Antes, el bloque para actualizar los objetos del gráfico se encontraba por debajo de la verificación de los cambios en el número de gráficos abiertos, lo cual no permitía que lo alcanzáramos si no había cambios en el número de gráficos abiertos:

//+------------------------------------------------------------------+
//| 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();
     }
  }
//+------------------------------------------------------------------+

La solución es trivial: basta con trasladar el bloque de código para actualizar los objetos del gráfico anterior; ahora lo colocamos antes de verificar el cambio en el número de gráficos abiertos en el terminal de 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();
    }
  }
//+------------------------------------------------------------------+

Ahora, antes de verificar el cambio en el número de gráficos abiertos (y salir del método si el número no ha cambiado), primero iteraremos por todos los objetos de gráfico en la lista de la colección y verificaremos el cambio de sus objetos de ventana en su método Refresh(), donde, a su vez, será llamado su propio método Refresh() para comprobar el número de indicadores en la ventana. Por consiguiente, primero haremos una verificación completa de todos los posibles cambios en el número de indicadores en las ventanas y las ventanas de indicadores en los gráficos, y solo entonces verificaremos el cambio en el número de gráficos abiertos.

Vamos a añadir a la clase de colección de objetos de gráfico los métodos necesarios para abrir un nuevo gráfico y cerrar uno existente.
En la sección pública de la clase, declaramos los dos nuevos 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);

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

Fuera del cuerpo de la clase, escribimos su implementación.

Método que abre un nuevo gráfico con el símbolo y el periodo 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);
  }
//+------------------------------------------------------------------+

Aquí, si el número de objetos de gráfico en la colección ha alcanzado el límite (CHARTS_MAX), resultará inútil intentar abrir un nuevo gráfico; informamos de ello y retornamos false. A continuación, si aún así podemos abrir un nuevo gráfico, llamaremos a la función ChartOpen() con los parámetros especificados del gráfico que se está abriendo, y en caso de error, informaremos de ello en el diario. Retornamos la bandera que indica que la función de apertura de un nuevo gráfico no ha devuelto cero.

Método para cerrar el gráfico indicado:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Aquí, si el intento de cerrar el gráfico especificado según su identificador no ha tenido éxito, imprimimos una entrada sobre ello en el diario.
El método retorna el resultado de la función ChartClose().

En el archivo \MQL5\Include\DoEasy\Engine.mqh del objeto principal de la biblioteca CEngine, añadimos los métodos para gestionar la colección de gráficos.

Aquí tenemos los dos métodos encargados de retornar las listas de objetos de gráfico según su símbolo y marco temporal

//--- 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);      }

han sido renombrados para coincidir con los métodos análogos de otras clases y desplazados un poco más arriba en la 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);      }

Ahora, añadimos un método que retorna el objeto de gráfico del último gráfico abierto, un método que retorna el número de objetos de gráfico en la lista de colección,
y dos métodos para abrir y para cerrar el 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 

El método ChartGetLastOpenedChart() simplemente retorna el puntero al objeto más reciente en la lista de la colección de objetos de gráfico,
mientras que el método ChartsTotal() retorna el tamaño de la lista de la colección de objetos de gráfico.
Los métodos ChartOpen() y ChartClose() retornan el resultado de los métodos Open() y Close() de la clase de colección de los gráficos, respectivamente.

Estos son todos los cambios y mejoras que tenemos planeados por hoy.


Simulación

Para las pruebas, vamos a tomar el asesor del artículo anterior y guardarlo en la carpeta nueva \MQL5\Experts\TestDoEasy\Part70\ con el nuevo nombre TestDoEasyPart70.mq5.

¿Qué vamos a hacer? Añadiremos nuevos botones con los siguientes iconos al panel del asesor:

La lógica para probar la nueva funcionalidad será la siguiente:

Bien, ahora escribiremos en el manejador OnInit() del asesor la activación del permiso para monitorear los eventos del ratón para el gráfico actual:

//--- 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);
  }
//+------------------------------------------------------------------+

Esto es necesario para que el programa pueda recibir mensajes sobre los eventos de movimiento y clicado en los botones del ratón (CHARTEVENT_MOUSE_MOVE).

En el manejador OnChartEvent(), al recibir un evento de movimiento del ratón, necesitamos obtener las coordenadas del cursor en el gráfico en píxeles, convertirlas (usando los métodos de la biblioteca creados) a los valores de tiempo, precio y número de ventana en que se halla el cursor, y luego convertir de nuevo el tiempo y el precio recién obtenidos a las coordenadas del cursor en el gráfico y mostrar todos estos valores en los comentarios del gráfico.

Vamos a implementar este procesamiento del evento de desplazamiento del 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(),")")
           ;
        }
     }
  }
//+------------------------------------------------------------------+

La lógica de procesamiento de un evento de movimiento del ratón se describe con detalle en el código OnChartEvent() del asesor.

En la función del asesor CreateButtons(), añadimos el código para crear los nuevos botones de panel:

//+------------------------------------------------------------------+
//| 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;
  }
//+------------------------------------------------------------------+

Aquí todo resulta muy sencillo: las coordenadas son calculadas para cada nuevo botón y el botón se crea utilizando la función ButtonCreate(), que recibe el nombre del objeto gráfico creado, así como sus coordenadas, anchura, altura, título y color. Si no podemos crear el botón, una alerta nos informará al respecto y se retornará false. En el manejador OnInit(), si la función de creación del panel ha retornado false, saldremos con el código de retorno INIT_FAILED.

En la función para procesar la pulsación de los botones PressButtonEvents(), añadimos el procesamiento de la pulsación de los nuevos botones:

//+------------------------------------------------------------------+
//| 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))
        {
         ...
         ...
         ...
       ...
       ...
     ...
     ...

El código de la función no se muestra al completo, solo los cambios añadidos.
Aquí, simplemente procesamos la pulsación de cada nuevo botón. La lógica es simple, así que no la vamos a describir: el lector podrá analizarla por sí mismo, tanto más que este manejador solo sirve como ejemplo para ver cómo podemos trabajar con los métodos de la biblioteca en nuestros programas. Siempre podrá plantear sus dudas sobre el código en los comentarios al artículo.

Estas son todas las mejoras que necesitábamos hacer en el nuevo asesor de prueba.

Vamos a compilar el asesor y a ejecutarlo en un gráfico de cualquier símbolo con las configuraciones "Usar el símbolo actual" y "Usar el marco temporal actual":


Antes de iniciar el asesor, deberemos asegurarnos de abrir un nuevo gráfico del símbolo GBPUSD y configurar su apariencia de forma diferente a los gráficos abiertos por defecto al usar la plantilla default.tpl, por ejemplo, así (el gráfico GBPUSD se ha abierto previamente):



Ahora podemos probar la nueva funcionalidad de la biblioteca presionando los botones del panel:


Con la apertura de cada nuevo gráfico, el asesor ha guardado la plantilla del símbolo GBPUSD previamente configurada por nosotros e inmediatamente la ha aplicado a cada gráfico recién abierto, hecho que ha registrado con entradas en el diario:

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)

Al cerrar los gráficos abiertos, ha ocurrido un error. El hecho es que la biblioteca actualiza el estado de los gráficos abiertos cada medio segundo.
Esto se ha establecido en el archivo 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

Simplemente hemos pulsado más de dos veces por segundo el botón de cierre del último gráfico abierto, por lo que se ha intentado cerrar el gráfico anterior ya cerrado, sobre el cual todavía había una entrada en la lista de la colección de gráficos. La frecuencia de actualización del estado actual de los gráficos abiertos, sus ventanas y los indicadores en ellas se puede ajustar cambiando la macrosustitución indicada. Para actualizar el entorno más rápido, deberemos reducir el valor de esta constante. En este caso, la carga sobre el procesador aumentará debido a la mayor frecuencia de las actualizaciones. Aquí deberemos encontrar el "justo medio", ya que esta funcionalidad todavía está destinada a controlar manualmente de los gráficos, por lo que la frecuencia de las actualizaciones es individual y, a veces, los errores al acceder a los gráficos que faltan no son críticos; simplemente hay que presionar el botón por segunda vez cuando se produzca la próxima actualización del entorno y la sincronización de la lista de objetos de gráfico en la biblioteca con su estado en el terminal de cliente.

Queda por probar la creación de capturas de pantalla del gráfico actual. Cada vez que presionemos el botón, se creará una captura de pantalla del gráfico con un tamaño determinado. Primera pulsación: captura de pantalla 800x600, segunda pulsación: captura de pantalla 750x562, tercera pulsación: captura de pantalla en el tamaño de gráfico actual:


Después de crear tres capturas de pantalla en diferentes resoluciones (sobre las cuales la biblioteca ha creado las entradas correspondientes en el diario),

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)

también miramos el contenido de la carpeta en la que se guardan estas capturas de pantalla.
Antes de tomar las capturas de pantalla, hemos movido el cursor por diferentes lugares de las dos ventanas del gráfico actual, y en los comentarios del gráfico se puede ver el tiempo, el precio, el número de subventana y las coordenadas X e Y del cursor en píxeles. La coordenada Y del cursor tiene dos valores. El primer valor muestra la coordenada Y respecto a las coordenadas iniciales de la ventana principal del símbolo, mientras que el segundo valor mostrado (entre paréntesis) indica el valor de la coordenada Y respecto al borde superior de la ventana donde se encuentra el cursor.

Como podemos ver, toda la funcionalidad planificada para hoy cumple correctamente su cometido.


¿Qué es lo próximo?

En el próximo artículo, implementaremos el seguimiento automático de los eventos de cambio de propiedades de los objetos de gráfico y sus ventanas.

Más abajo se adjuntan todos los archivos de la versión actual de la biblioteca y el archivo del asesor de prueba para MQL5. Puede descargarlo todo y ponerlo a prueba por sí mismo.
Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.

Volver al contenido

*Artículos de esta serie:

Otras clases en la biblioteca DoEasy (Parte 67): Clase de objeto de gráfico
Otras clases en la biblioteca DoEasy (Parte 68): Clase de objeto de ventana de gráfico y clases de objetos de indicador en la ventana del gráfico
Otras clases en la biblioteca DoEasy (Parte 69): Clases de colección de objetos de gráfico