Gráficos en la biblioteca DoEasy (Parte 95): Elementos de control de los objetos gráficos compuestos
Contenido
- Concepto
- Mejorando las clases de la biblioteca
- Clase de instrumental del objeto gráfico estándar extendido
- Simulación
- ¿Qué es lo próximo?
Concepto
Continuamos con el desarrollo de los objetos gráficos compuestos. Estos son objetos gráficos estándar que constan de varios de ellos y se combinan en un objeto gráfico. Los objetos gráficos incluidos en el objeto compuesto se definen en la biblioteca como objetos gráficos estándar extendidos. Dichos objetos tienen algunas propiedades y funcionalidades adicionales que les permiten incorporar objetos gráficos, y también ser incorporados a otros.
El concepto de objeto gráfico compuesto requiere una funcionalidad que le permita mantener el objeto en el lugar donde está adjunto a otro objeto y ajustar su ubicación cuando el objeto principal cambie o se mueva.
En el último artículo, comenzamos a crear manejadores de eventos para objetos gráficos compuestos; asimismo, procesamos la eliminación de un objeto gráfico compuesto y comenzamos a desarrollar un manejador para moverlo.
Hoy nos desviaremos un poco del tema del desplazamiento de objetos gráficos compuestos y crearemos un manejador de eventos de cambio del gráfico en el que se encuentra el objeto gráfico compuesto; también trabajaremos con los objetos de gestión de objetos gráficos compuestos.
¿Porqué hacemos así? Hemos planificado la creación en tiempo real de objetos gráficos compuestos, vinculando un objeto dependiente al objeto básico al arrastrar el objeto dependiente al objeto básico. El objeto gráfico básico monitoreará la aproximación de otro objeto con el ratón, y a cierta distancia de uno de sus puntos de anclaje al gráfico, se activará el mecanismo para unir el objeto que estamos acercando con el ratón. En este caso, se mostrarán las líneas que conectan el punto de anclaje del objeto adjunto con el punto de anclaje del objeto básico, lo cual mostrará visualmente la preparación del objeto arrastrado para fijarse al objeto básico. Para lograr esto, cada punto de anclaje del objeto gráfico deberá tener un objeto de formulario de cierto tamaño. La entrada en el área de los objetos de formulario activará el mecanismo de anclaje, mientras que las líneas que indican que los objetos están listos para la interacción, se mostrarán en el formulario mismo. Tales formularios estarán presentes de forma invisible en cada punto de pivote del objeto gráfico. El tamaño del área se podrá ver solo en casos concretos (para realizar la depuración), habilitando para ello el dibujado de un rectángulo a lo largo de los bordes del formulario:
Además, en el formulario se mostrarán los puntos de anclaje del objeto gráfico que también aparecerán solo cuando el cursor del ratón entre en el área activa del formulario. Por ello, moveremos y modificaremos el objeto gráfico extendido no seleccionándolo con un clic del ratón, sino colocando el cursor del mismo sobre el área del formulario. Tan pronto como pasemos el cursor sobre el área activa del formulario (delineada por los rectángulos en la figura de arriba), aparecerán unas marcas en el punto de anclaje del objeto gráfico (en la figura de arriba es un punto azul en el centro de el círculo); si tomamos el formulario con el ratón y comenzamos a moverlo, el punto de anclaje correspondiente del objeto gráfico se moverá tras el cursor, lo cual provocará la modificación del objeto en sí y, en consecuencia, del objeto gráfico compuesto.
Si el cursor del ratón entra en el área activa del formulario con el botón del ratón presionado, esto significará (con verificación) que estamos arrojando a este formulario otro objeto gráfico, lo cual activará el mecanismo para vincular un objeto a otro. Así, con la ayuda de estos formularios, resolveremos varios problemas a la vez.
Hoy, no vamos a vincular un objeto a otro; faltan algunas cosas para comenzar a crear esta funcionalidad. No obstante, crearemos los formularios en sí: los adjuntaremos a los puntos de anclaje del objeto gráfico y crearemos un mecanismo para moverlos a las coordenadas de los puntos de pivote del objeto cuando el gráfico cambie, por ejemplo, al moverse y modificarse su escala de visualización. Hay que hacer esto porque el objeto de formulario tiene sus coordenadas en píxeles de la pantalla, mientras que la mayoría de los objetos gráficos tienen valores de tiempo/precio.
Mejorando las clases de la biblioteca
En el archivo \MQL5\Include\DoEasy\Data.mqh, escribimos los índices de los nuevos mensajes:
//--- CLinkedPivotPoint MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_X, // Not a single pivot point is set for the object along the X axis MSG_GRAPH_OBJ_EXT_NOT_ANY_PIVOTS_Y, // Not a single pivot point is set for the object along the Y axis MSG_GRAPH_OBJ_EXT_NOT_ATACHED_TO_BASE, // The object is not attached to the basic graphical object MSG_GRAPH_OBJ_EXT_FAILED_CREATE_PP_DATA_OBJ, // Failed to create a data object for the X and Y pivot points MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_X, // Number of base object pivot points for calculating the X coordinate: MSG_GRAPH_OBJ_EXT_NUM_BASE_PP_TO_SET_Y, // Number of base object pivot points for calculating the Y coordinate: //--- CGStdGraphObjExtToolkit MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA, // Failed to change the size of the pivot point time data array MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA, // Failed to change the size of the pivot point price data array MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM, // Failed to create a form object to manage a pivot point }; //+------------------------------------------------------------------+
y los textos de los mensajes que se corresponden con los índices nuevamente añadidos:
//--- CLinkedPivotPoint {"Для объекта не установлено ни одной опорной точки по оси X","The object does not have any pivot points set along the x-axis"}, {"Для объекта не установлено ни одной опорной точки по оси Y","The object does not have any pivot points set along the y-axis"}, {"Объект не привязан к базовому графическому объекту","The object is not attached to the base graphical object"}, {"Не удалось создать объект данных опорной точки X и Y.","Failed to create X and Y reference point data object"}, {"Количество опорных точек базового объекта для расчёта координаты X: ","Number of reference points of the base object to set the X coordinate: "}, {"Количество опорных точек базового объекта для расчёта координаты Y: ","Number of reference points of the base object to set the Y coordinate: "}, //--- CGStdGraphObjExtToolkit {"Не удалось изменить размер массива данных времени опорной точки","Failed to resize pivot point time data array"}, {"Не удалось изменить размер массива данных цены опорной точки","Failed to resize pivot point price data array"}, {"Не удалось создать объект-форму для контроля опорной точки","Failed to create form object to control pivot point"}, }; //+---------------------------------------------------------------------+
En el archivo \MQL5\Include\DoEasy\Defines.mqh, cambiamos la macrosustitución
#define CLR_DEFAULT (0xFF000000) // Default symbol background color in the navigator
por otra más comprensible
#define CLR_MW_DEFAULT (0xFF000000) // Default symbol background color in the Market Watch
y la macrosustitución
#define NULL_COLOR (0x00FFFFFF) // Zero for the canvas with the alpha channel
por otra más comprensible
#define CLR_CANV_NULL (0x00FFFFFF) // Zero for the canvas with the alpha channel
y escribimos nuevas macrosustituciones para establecer los valores por defecto de los objetos de formulario que crearemos hoy:
//--- Graphical object parameters #define PROGRAM_OBJ_MAX_ID (10000) // Maximum value of an ID of a graphical object belonging to a program #define CTRL_POINT_SIZE (5) // Radius of the control point on the form for managing graphical object pivot points #define CTRL_FORM_SIZE (40) // Size of the control point form for managing graphical object pivot points //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+
Como hemos cambiado los nombres de las macrosustituciones, reemplazaremos los nombres obsoletos en todos los archivos.
Simplemente presionaremos la combinación de teclas Ctrl+Shift+H, introduciremos los siguientes valores en los campos y marcaremos las casillas como en la figura:
Después, presionaremos el botón Replace in Files (Reemplazar en los archivos). El editor buscará todas las carpetas en la biblioteca y realizará la sustitución en ellas.
Haremos lo mismo para reemplazar "NULL_COLOR" por "CLR_CANV_NULL"
Estos nombres para las macrosustituciones resultan más descriptivos, y no nos obligarán a recordar en el futuro cuál es la función de cada uno (introdujimos por error CLR_DEFAULT para establecer un fondo transparente para el lienzo y buscamos durante mucho tiempo por qué no era transparente)
Además de estos reemplazos, hemos realizado ajustes menores en todos los archivos de clase de los descendientes del objeto gráfico básico (simplemente hemos añadido una coma al texto del método que registra la descripción breve del objeto):
//+------------------------------------------------------------------+ //| Display a short description of the object in the journal | //+------------------------------------------------------------------+ void CGStdArrowBuyObj::PrintShort(const bool dash=false,const bool symbol=false) { ::Print ( (dash ? " - " : "")+this.Header(symbol)," \"",CGBaseObj::Name(),"\": ID ",(string)this.GetProperty(GRAPH_OBJ_PROP_ID,0), ", ",::TimeToString(CGBaseObj::TimeCreate(),TIME_DATE|TIME_MINUTES|TIME_SECONDS) ); } //+------------------------------------------------------------------+
Se trata de una mejora relacionada puramente con el "diseño".
La hemos implementado en todos los archivos de la carpeta \MQL5\Include\DoEasy\Objects\Graph\Standard\; usted mismo podrá verlo en los archivos adjuntos al artículo.
Clase de instrumental del objeto gráfico estándar extendido
Bien, vamos a proceder a crear el instrumental necesario para trabajar con objetos gráficos extendidos. Nos referimos a la clase en la que escribiremos los métodos para crear los objetos de formulario y trabajar con ellos. Cada objeto gráfico extendido contendrá un puntero a un objeto de esta clase. Si es necesario (si hablamos de un objeto básico dentro de un objeto gráfico compuesto), el objeto de clase se creará dinámicamente cuando se cree el objeto extendido, y será eliminado cuando este se elimine.
Los parámetros necesarios del objeto gráfico básico (sus coordenadas, tipo, nombre, etc.) se transferirán al objeto, y dentro del objeto se rastrearán las coordenadas del objeto básico y se corregirán las coordenadas de los objetos de formulario. En consecuencia, los objetos de formulario finalmente nos permitirán controlar el objeto básico.
Pero, vayamos por orden...
En el directorio de la biblioteca \MQL5\Include\DoEasy\Objects\Graph\, creamos una nueva carpeta Extend\, y en ella, un nuevo archivo CGStdGraphObjExtToolkit.mqh de la clase CGStdGraphObjExtToolkit, heredada de la clase básica CObject, para construir la biblioteca estándar MQL5 :
//+------------------------------------------------------------------+ //| CGStdGraphObjExtToolkit.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\..\Graph\Form.mqh" //+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { }
El archivo de clase del objeto de formulario deberá estar conectado al archivo de clase.
En la sección privada de la clase, declararemos las variables en las que almacenaremos todas las propiedades necesarias del objeto básico,
las propiedades para crear formularios, la lista para almacenarlos y los métodos para crear un objeto de formulario y retornar sus coordenadas de pantalla :
//+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { private: long m_base_chart_id; // Base graphical object chart ID int m_base_subwindow; // Base graphical object chart subwindow ENUM_OBJECT m_base_type; // Base object type string m_base_name; // Base object name int m_base_pivots; // Number of base object reference points datetime m_base_time[]; // Time array of base object reference points double m_base_price[]; // Price array of base object reference points int m_base_x; // Base object X coordinate int m_base_y; // Base object Y coordinate int m_ctrl_form_size; // Size of forms for managing reference points int m_shift; // Shift coordinates for adjusting the form location CArrayObj m_list_forms; // List of form objects for managing reference points //--- Create a form object on a base object reference point CForm *CreateNewControlPointForm(const int index); //--- Return X and Y screen coordinates of the specified reference point of the graphical object bool GetControlPointCoordXY(const int index,int &x,int &y); public:
Usaremos matrices para almacenar las coordenadas de precio y tiempo, ya que un objeto gráfico puede tener varios puntos de pivote; por ello, las coordenadas de cada punto se almacenarán en las celdas correspondientes de la matriz. La coordenada del primer punto estará en el índice de matriz 0, la coordenada del segundo punto estará en el índice de matriz 1, la coordenada del tercero estará en el índice 2, etc.
Necesitamos el desplazamiento de las coordenadas del formulario para colocar con precisión el formulario en el centro del punto de pivote del objeto; este desplazamiento supondrá la mitad de su tamaño. En este caso, si el tamaño del formulario se establecerá como un múltiplo de dos, por ejemplo, 10 se corregirá en 1 más, es decir, 11. Esto es necesario para que el formulario se pueda colocar exactamente en el centro del punto de pivote del objeto gráfico y no tenga el tamaño de uno de los lados más un píxel del tamaño del lado opuesto.
En la lista de formularios, almacenaremos todos los formularios creados, y desde ella accederemos a estos con la ayuda de un puntero. El método para calcular las coordenadas de la pantalla lo necesitaremos para saber en qué coordenadas de la pantalla colocar el formulario para que quede exactamente en el punto de pivote del objeto gráfico.
En la sección pública de la clase, declaramos todos los métodos necesarios para trabajar con la clase, que analizaremos a continuación:
public: //--- Set the parameters of the base object of a composite graphical object void SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]); //--- Set the base object (1) time, (2) price, (3) time and price coordinates void SetBaseObjTime(const datetime time,const int index); void SetBaseObjPrice(const double price,const int index); void SetBaseObjTimePrice(const datetime time,const double price,const int index); //--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates void SetBaseObjCoordX(const int value) { this.m_base_x=value; } void SetBaseObjCoordY(const int value) { this.m_base_y=value; } void SetBaseObjCoordXY(const int value_x,const int value_y) { this.m_base_x=value_x; this.m_base_y=value_y; } //--- (1) Set and (2) return the size of the form of pivot point management control points void SetControlFormSize(const int size); int GetControlFormSize(void) const { return this.m_ctrl_form_size; } //--- Return the pointer to the pivot point form by (1) index and (2) name CForm *GetControlPointForm(const int index) { return this.m_list_forms.At(index); } CForm *GetControlPointForm(const string name,int &index); //--- Return the number of the base object pivot points int GetNumPivotsBaseObj(void) const { return this.m_base_pivots; } //--- Create form objects on the base object pivot points bool CreateAllControlPointForm(void); //--- Remove all form objects from the list void DeleteAllControlPointForm(void); //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Constructor/destructor CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_list_forms.Clear(); this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price); this.CreateAllControlPointForm(); } CGStdGraphObjExtToolkit(){;} ~CGStdGraphObjExtToolkit(){;} }; //+------------------------------------------------------------------+
En el constructor de clase, limpiamos la lista de objetos de formulario, asignamos a las variables de clase todos los valores del objeto básico (transmitidos en los parámetros del constructor) necesarios para el funcionamiento de la clase, y creamos en cada punto de pivote del objeto gráfico básico los objetos de formulario necesarios para controlar e interactuar con el objeto básico.
Método que establece los parámetros del objeto básico de un objeto gráfico compuesto:
//+------------------------------------------------------------------+ //| Set the base object parameters of the | //| composite graphical object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_base_chart_id=base_chart_id; // Base graphical object chart ID this.m_base_subwindow=base_subwindow; // Base graphical object chart subwindow this.m_base_type=base_type; // Base object type this.m_base_name=base_name; // Base object name this.m_base_pivots=base_pivots; // Number of base object reference points this.m_base_x=base_x; // Base object X coordinate this.m_base_y=base_y; // Base object Y coordinate this.SetControlFormSize(ctrl_form_size); // Size of forms for managing reference points if(this.m_base_type==OBJ_LABEL || this.m_base_type==OBJ_BUTTON || this.m_base_type==OBJ_BITMAP_LABEL || this.m_base_type==OBJ_EDIT || this.m_base_type==OBJ_RECTANGLE_LABEL || this.m_base_type==OBJ_CHART) return; if(::ArraySize(base_time)==0) { CMessage::ToLog(DFUN+"base_time: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY); return; } if(::ArraySize(base_price)==0) { CMessage::ToLog(DFUN+"base_price: ",MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY); return; } if(::ArrayResize(this.m_base_time,this.m_base_pivots)!=this.m_base_pivots) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA); return; } if(::ArrayResize(this.m_base_price,this.m_base_pivots)!=this.m_base_pivots) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA); return; } for(int i=0;i<this.m_base_pivots;i++) { this.m_base_time[i]=base_time[i]; // Time (i) of the base object pivot point this.m_base_price[i]=base_price[i]; // Price (i) of the base object pivot point } } //+------------------------------------------------------------------+
Transmitimos al método todos los valores de las propiedades del objeto gráfico básico necesarios para que la clase funcione. A continuación, verificamos el tipo del objeto básico y, si es un objeto que no se ha construido según las coordenadas de precio/tiempo, salimos del método: por el momento, no vamos a trabajar con tales objetos, simplemente no los procesaremos. A continuación, comprobamos los tamaños de las matrices con las coordenadas del objeto básico que se han transmitido al método y, si son de tamaño cero (se ha transmitido una matriz vacía al método), informaremos sobre ello y saldremos del método. Luego cambiamos el tamaño de las matrices internas de coordenadas según las coordenadas transmitidas, y si el cambio de tamaño no ha funcionado, informaremos de ello y saldremos. Finalmente, copiamos las matrices de entrada elemento por elemento en las matrices internas.
Método que asigna a los puntos de pivote el tamaño de los puntos de referencia de control:
//+------------------------------------------------------------------+ //|Set the size of reference points for managing pivot points | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetControlFormSize(const int size) { this.m_ctrl_form_size=(size>254 ? 255 : size<5 ? 5 : size%2==0 ? size+1 : size); this.m_shift=(int)ceil(m_ctrl_form_size/2)+1; } //+------------------------------------------------------------------+
Transmitimos al método el tamaño de formulario requerido. Si el tamaño es superior a 254, lo estableceremos en 255 (valor impar); si el tamaño transmitido es inferior a 5, lo estableceremos en 5 (este será el valor mínimo del tamaño del formulario). De lo contrario, si el tamaño transmitido es múltiplo de dos, añaderemos uno y lo utilizaremos; si ninguno de los valores verificados es verdadero, usaremos el tamaño transmitido al método.
A continuación, calcularemos el valor del cambio de coordenadas (porque el formulario debe encontrarse de forma que el punto de pivote del objeto gráfico esté exactamente en su centro, y para ello deberemos restar el valor del desplazamiento al valor de la coordenada). Dividimos por dos el tamaño del formulario calculado, tomamos el número entero más próximo por arriba y añadimos uno.
Método que establece la coordenada de tiempo del objeto básico:
//+------------------------------------------------------------------+ //| Set the time coordinate of the base object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjTime(const datetime time,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_time[index]=time; } //+------------------------------------------------------------------+
Transmitimos al método el valor de tiempo del punto de pivote y el índice del punto de pivote del objeto. Si el índice es mayor que el número de puntos de pivote en el objeto, informaremos sobre la solicitud fuera de los límites y saldremos. Como resultado, en la matriz de tiempo, en la celda correspondiente al índice, introduciremos el valor de tiempo transmitido al método.
El método es necesario para señalar el objeto de clase de tiempo del objeto básico cuando cambia.
Método que establece la coordenada de precio del objeto básico:
//+------------------------------------------------------------------+ //| Set the coordinate of the base object price | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjPrice(const double price,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_price[index]=price; } //+------------------------------------------------------------------+
El método es idéntico al analizado anteriormente, salvo que aquí introducimos en la matriz de precios de la clase el precio indicado por el índice del punto de pivote del objeto básico.
Método que establece las coordenadas de tiempo y precio del objeto subordinado:
//+------------------------------------------------------------------+ //| Set the time and price coordinates of the base object | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetBaseObjTimePrice(const datetime time,const double price,const int index) { if(index>this.m_base_pivots-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY); return; } this.m_base_time[index]=time; this.m_base_price[index]=price; } //+------------------------------------------------------------------+
El método es idéntico a los dos anteriores, pero establece tanto el precio como el tiempo en las matrices de clase.
Método que retorna las coordenadas X e Y del punto de pivote indicado del objeto gráfico en coordenadas de pantalla:
//+------------------------------------------------------------------+ //| Return the X and Y coordinates of the specified pivot point | //| of the graphical object in screen coordinates | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y) { switch(this.m_base_type) { case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : x=this.m_base_x; y=this.m_base_y; break; default: if(!::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y)) { x=0; y=0; return false; } } return true; } //+------------------------------------------------------------------+
Transmitimos al método el índice del punto de pivote necesario del objeto gráfico básico para el que deseamos obtener las coordenadas de pantalla (en píxeles desde la esquina superior izquierda de la pantalla), así como las dos variables por enlace en las que se escribirán las coordinadas de pantalla del formulario. Si el objeto ya está ubicado en las coordenadas de la pantalla, también las retornaremos.
Si el objeto está ubicado en las coordenadas de precio/tiempo, entonces las calcularemos usando la función ChartTimePriceToXY() y, si no ha sido posible convertir las coordenadas a coordenadas de pantalla, estableceremos las coordenadas en cero y retornaremos false.
Como resultado, retornaremos true.
Método que retorna un puntero al formulario del punto de pivote según el nombre:
//+------------------------------------------------------------------+ //| Return the pointer to the pivot point form by name | //+------------------------------------------------------------------+ CForm *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_forms.Total();i++) { CForm *form=this.m_list_forms.At(i); if(form==NULL) continue; if(form.Name()==name) { index=i; return form; } } return NULL; } //+------------------------------------------------------------------+
Transmitimos al método el nombre del formulario requerido y una variable por enlace, en la cual escribiremos el índice del formulario encontrado en la lista de objetos de formulario.
En un ciclo a través de la lista de objetos de formulario, obtenemos el siguiente objeto y, si su nombre coincide con el que estamos buscando, escribiremos el índice del ciclo en una variable y retornaremos el puntero al objeto encontrado. Al final del ciclo, retornaremos NULL : no se ha encontrado el formulario, y el índice tendrá el valor -1 escrito antes de que comenzara el ciclo.
Método que crea un objeto de formulario en el punto de pivote del objeto básico:
//+------------------------------------------------------------------+ //| Create a form object on a base object reference point | //+------------------------------------------------------------------+ CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_TKPP_"+(index<this.m_base_pivots ? (string)index : "X"); CForm *form=this.GetControlPointForm(index); if(form!=NULL) return NULL; int x=0, y=0; if(!this.GetControlPointCoordXY(index,x,y)) return NULL; return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); } //+------------------------------------------------------------------+
Transmitimos al método el índice del punto de pivote en el que debemos crear el objeto de formulario.
A continuación, creamos un nombre para el objeto de formulario que conste del nombre del objeto básico + la abreviatura de "ToolKit Pivot Point" (_TKPP) + el índice del punto de pivote. Al crear una descripción del índice, comprobaremos su valor y, si es menor que el número de puntos de pivote del objeto básico (el conteo comienza desde cero), usaremos la representación de línea del índice transmitido al método. De lo contrario, utilizaremos el icono "X". ¿Para qué necesitamos esto? En el futuro, podremos fijar objetos subordinados al objeto básico no solo en sus puntos de pivote, sino también entre estos. Además, para desplazar el objeto completo, necesitaremos crear un formulario de control en el centro de la línea del objeto básico más allá de la cual moveremos el objeto completo. Por consiguiente, en el nombre del formulario, deberemos prever de inmediato la posibilidad de crear un formulario no solo para los puntos de pivote, sino también para otros puntos.
A continuación, verificamos la presencia de un formulario en la lista según el índice transmitido al método, y si ya hay un objeto de formulario en este índice en la lista (el puntero a él no es igual a NULL), retornaremos NULL.
Luego transformamos las coordenadas del punto de pivote según su índice en coordenadas de pantalla y retornamos el resultado de la creación del objeto de formulario en las coordenadas obtenidas. En este caso, restaremos a ambas coordenadas el valor de su desplazamiento, para así posicionar de forma precisa el centro del formulario en el punto de pivote.
Podríamos haber configurado el punto de anclaje del formulario para que esté centrado, pero no debemos olvidar que habíamos acordado en la biblioteca que el punto de anclaje de todos los formularios sería el mismo, en la parte superior izquierda del formulario. Por ello, donde sea necesario, usaremos el desplazamiento para colocar los objetos de formulario.
Método que crea objetos de formulario en los puntos de pivote del objeto básico:
//+------------------------------------------------------------------+ //| Create form objects on the base object pivot points | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void) { bool res=true; //--- In the loop by the number of base object pivot points for(int i=0;i<this.m_base_pivots;i++) { //--- Create a new form object on the current pivot point corresponding to the loop index CForm *form=this.CreateNewControlPointForm(i); //--- If failed to create the form, inform of that and add 'false' to the final result if(form==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM); res &=false; } //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result if(!this.m_list_forms.Add(form)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); delete form; res &=false; } //--- Set all the necessary properties for the created form object form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Object is created programmatically form.SetActive(true); // Form object is active form.SetMovable(true); // Movable object form.SetActiveAreaShift(0,0,0,0); // Object active area - the entire form form.SetFlagSelected(false,false); // Object is not selected form.SetFlagSelectable(false,false); // Object cannot be selected by mouse form.Erase(CLR_CANV_NULL,0); // Fill in the form with transparent color and set the full transparency //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location form.DrawCircle((int)floor(form.Width()/2),(int)floor(form.Height()/2),CTRL_POINT_SIZE,clrDodgerBlue); // Draw a circle in the form center form.DrawCircleFill((int)floor(form.Width()/2),(int)floor(form.Height()/2),2,clrDodgerBlue); // Draw a point in the form center form.Done(); // Save the initial form object state (its appearance) } //--- Redraw the chart for displaying changes (if successful) and return the final result if(res) ::ChartRedraw(this.m_base_chart_id); return res; } //+------------------------------------------------------------------+
La lógica completa está descrita en los comentarios al código. En resumen: en un ciclo por el número de puntos de pivote del objeto básico, creamos un nuevo objeto de formulario para cada punto de pivote, lo añadimos a la lista de objetos de formulario y asignamos a cada formulario las coordenadas del punto de pivote correspondiente del objeto básico. Cada formulario dibuja un círculo en el centro y un punto para indicar que es un objeto de control del punto de pivote del objeto básico.
Como estaba previsto, dichos objetos serán inicialmente invisibles y aparecerán solo cuando el cursor del ratón entre en el área del formulario. Pero por ahora, los haremos visibles, para poner a prueba su comportamiento cuando cambie el gráfico. Ya en próximos artículos, los haremos según lo previsto: ocultos de inicio y posteriormente visibles cuando el cursor entre en su área activa, que tendrá el tamaño completo del objeto de formulario.
Método que elimina todos los objetos de formulario de la lista:
//+------------------------------------------------------------------+ //| Remove all form objects from the list | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DeleteAllControlPointForm(void) { this.m_list_forms.Clear(); } //+------------------------------------------------------------------+
Simplemente usaremos el método Clear(), que borra totalmente la lista completa.
En el manejador de eventos, procesaremos los eventos de los objetos de formulario según el evento ocurrido:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { if(id==CHARTEVENT_CHART_CHANGE) { for(int i=0;i<this.m_list_forms.Total();i++) { CForm *form=this.m_list_forms.At(i); if(form==NULL) continue; int x=0, y=0; if(!this.GetControlPointCoordXY(i,x,y)) continue; form.SetCoordX(x-this.m_shift); form.SetCoordY(y-this.m_shift); form.Update(); } ::ChartRedraw(this.m_base_chart_id); } } //+------------------------------------------------------------------+
Por el momento, solo estamos procesando el evento de cambio del gráfico. En un ciclo a través de todos los objetos de formulario, obtenemos el siguiente formulario de la lista, y si no ha sido posible obtener sus coordenadas de pantalla según el punto de pivote en el que se dibuja, pasaremos al siguiente formulario. Establecemos en el formulario las nuevas coordenadas de pantalla y actualizamos el formulario. Al final del ciclo, redibujamos el gráfico para mostrar los cambios.
Como el objeto de instrumental del objeto gráfico estándar extendido se almacenará en el objeto de clase del objeto gráfico estándar, deberemos modificar esta clase en el archivo \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.
En primer lugar, incluiremos en el archivo los archivos de las clases del objeto de formulario y el objeto de instrumental recién creado del objeto gráfico estándar extendido:
//+------------------------------------------------------------------+ //| GStdGraphObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\GBaseObj.mqh" #include "..\..\..\Services\Properties.mqh" #include "..\..\Graph\Form.mqh" #include "..\..\Graph\Extend\CGStdGraphObjExtToolkit.mqh" //+------------------------------------------------------------------+ //| Class of the dependent object pivot point data | //+------------------------------------------------------------------+
En la clase del objeto gráfico estándar abstracto, en su sección privada, declaramos un puntero al objeto de instrumental del objeto gráfico estándar extendido:
//+------------------------------------------------------------------+ //| The class of the abstract standard graphical object | //+------------------------------------------------------------------+ class CGStdGraphObj : public CGBaseObj { private: CArrayObj m_list; // List of subordinate graphical objects CProperties *Prop; // Pointer to the property object CLinkedPivotPoint m_linked_pivots; // Linked pivot points CGStdGraphObjExtToolkit *ExtToolkit; // Pointer to the extended graphical object toolkit int m_pivots; // Number of object reference points //--- Read and set (1) the time and (2) the price of the specified object pivot point void SetTimePivot(const int index); void SetPricePivot(const int index); //--- Read and set (1) color, (2) style, (3) width, (4) value, (5) text of the specified object level void SetLevelColor(const int index); void SetLevelStyle(const int index); void SetLevelWidth(const int index); void SetLevelValue(const int index); void SetLevelText(const int index); //--- Read and set the BMP file name for the "Bitmap Level" object. Index: 0 - ON, 1 - OFF void SetBMPFile(const int index); public:
En la sección pública, escribimos un método que retorna el puntero al objeto de instrumental:
public: //--- Set object's (1) integer, (2) real and (3) string properties void SetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Curr.SetLong(property,index,value); } void SetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value) { this.Prop.Curr.SetDouble(property,index,value); } void SetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value) { this.Prop.Curr.SetString(property,index,value); } //--- Return object’s (1) integer, (2) real and (3) string property from the properties array long GetProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Curr.GetLong(property,index); } double GetProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Curr.GetDouble(property,index); } string GetProperty(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Curr.GetString(property,index); } //--- Set object's previous (1) integer, (2) real and (3) string properties void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index,long value) { this.Prop.Prev.SetLong(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index,double value){ this.Prop.Prev.SetDouble(property,index,value); } void SetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index,string value){ this.Prop.Prev.SetString(property,index,value); } //--- Return object’s (1) integer, (2) real and (3) string property from the previous properties array long GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_INTEGER property,int index) const { return this.Prop.Prev.GetLong(property,index); } double GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_DOUBLE property,int index) const { return this.Prop.Prev.GetDouble(property,index); } string GetPropertyPrev(ENUM_GRAPH_OBJ_PROP_STRING property,int index) const { return this.Prop.Prev.GetString(property,index); } //--- Return (1) itself, (2) properties and (3) the change history CGStdGraphObj *GetObject(void) { return &this; } CProperties *Properties(void) { return this.Prop; } CChangeHistory *History(void) { return this.Prop.History;} CGStdGraphObjExtToolkit *GetExtToolkit(void) { return this.ExtToolkit; } //--- Return the flag of the object supporting this property virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_DOUBLE property) { return true; } virtual bool SupportProperty(ENUM_GRAPH_OBJ_PROP_STRING property) { return true; }
En la sección pública de la clase, declararemos el manejador de eventos del objeto gráfico; en el constructor predeterminado, estableceremos el puntero al objeto de instrumental como NULL, y en el destructor de la clase, verificaremos la validez del puntero. Después, eliminaremos todos los formularios del objeto de instrumental, y luego el propio objeto:
private: //--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property void SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier); void SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier); void SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier); public: //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Default constructor CGStdGraphObj(){ this.m_type=OBJECT_DE_TYPE_GSTD_OBJ; this.m_species=WRONG_VALUE; this.ExtToolkit=NULL; } //--- Destructor ~CGStdGraphObj() { if(this.Prop!=NULL) delete this.Prop; if(this.ExtToolkit!=NULL) { this.ExtToolkit.DeleteAllControlPointForm(); delete this.ExtToolkit; } } protected: //--- Protected parametric constructor CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_ELEMENT_TYPE elm_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_SPECIES species, const long chart_id, const int pivots, const string name); public: //+--------------------------------------------------------------------+ //|Methods of simplified access and setting graphical object properties| //+--------------------------------------------------------------------+
En el bloque de métodos para el acceso simplificado y la configuración de las propiedades del objeto gráfico, escribimos el método que retorna el número de puntos de pivote del objeto gráfico:
public: //+--------------------------------------------------------------------+ //|Methods of simplified access and setting graphical object properties| //+--------------------------------------------------------------------+ //--- Number of object reference points int Pivots(void) const { return this.m_pivots; } //--- Object index in the list int Number(void) const { return (int)this.GetProperty(GRAPH_OBJ_PROP_NUM,0); } void SetNumber(const int number) { this.SetProperty(GRAPH_OBJ_PROP_NUM,0,number); }
En el constructor paramétrico protegido, comprobamos el tipo del elemento gráfico y, si es un objeto gráfico extendido, crearemos un nuevo objeto de instrumental y almacenaremos el puntero a este en la variable ExtToolkit. Al final del listado del constructor, inicializamos el objeto de instrumental:
//+------------------------------------------------------------------+ //| Protected parametric constructor | //+------------------------------------------------------------------+ CGStdGraphObj::CGStdGraphObj(const ENUM_OBJECT_DE_TYPE obj_type, const ENUM_GRAPH_ELEMENT_TYPE elm_type, const ENUM_GRAPH_OBJ_BELONG belong, const ENUM_GRAPH_OBJ_SPECIES species, const long chart_id,const int pivots, const string name) { //--- Create the property object with the default values this.Prop=new CProperties(GRAPH_OBJ_PROP_INTEGER_TOTAL,GRAPH_OBJ_PROP_DOUBLE_TOTAL,GRAPH_OBJ_PROP_STRING_TOTAL); this.ExtToolkit=(elm_type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? new CGStdGraphObjExtToolkit() : NULL); //--- Set the number of pivot points and object levels this.m_pivots=pivots; int levels=(int)::ObjectGetInteger(chart_id,name,OBJPROP_LEVELS); //--- Set the property array dimensionalities according to the number of pivot points and levels this.Prop.SetSizeRange(GRAPH_OBJ_PROP_TIME,this.m_pivots); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_PRICE,this.m_pivots); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELCOLOR,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELSTYLE,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELWIDTH,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELVALUE,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_LEVELTEXT,levels); this.Prop.SetSizeRange(GRAPH_OBJ_PROP_BMPFILE,2); //--- Set the object (1) type, type of graphical (2) object, (3) element, (4) subwindow affiliation and (5) index, as well as (6) chart symbol Digits this.m_type=obj_type; this.SetName(name); CGBaseObj::SetChartID(chart_id); CGBaseObj::SetTypeGraphObject(CGBaseObj::GraphObjectType(obj_type)); CGBaseObj::SetTypeElement(elm_type); CGBaseObj::SetBelong(belong); CGBaseObj::SetSpecies(species); CGBaseObj::SetSubwindow(chart_id,name); CGBaseObj::SetDigits((int)::SymbolInfoInteger(::ChartSymbol(chart_id),SYMBOL_DIGITS)); //--- Save the integer properties inherent in all graphical objects but not present in the current one this.SetProperty(GRAPH_OBJ_PROP_CHART_ID,0,CGBaseObj::ChartID()); // Chart ID this.SetProperty(GRAPH_OBJ_PROP_WND_NUM,0,CGBaseObj::SubWindow()); // Chart subwindow index this.SetProperty(GRAPH_OBJ_PROP_TYPE,0,CGBaseObj::TypeGraphObject()); // Graphical object type (ENUM_OBJECT) this.SetProperty(GRAPH_OBJ_PROP_ELEMENT_TYPE,0,CGBaseObj::TypeGraphElement()); // Graphical element type (ENUM_GRAPH_ELEMENT_TYPE) this.SetProperty(GRAPH_OBJ_PROP_BELONG,0,CGBaseObj::Belong()); // Graphical object affiliation this.SetProperty(GRAPH_OBJ_PROP_SPECIES,0,CGBaseObj::Species()); // Graphical object species this.SetProperty(GRAPH_OBJ_PROP_GROUP,0,0); // Graphical object group this.SetProperty(GRAPH_OBJ_PROP_ID,0,0); // Object ID this.SetProperty(GRAPH_OBJ_PROP_BASE_ID,0,0); // Base object ID this.SetProperty(GRAPH_OBJ_PROP_NUM,0,0); // Object index in the list this.SetProperty(GRAPH_OBJ_PROP_CHANGE_HISTORY,0,false); // Flag of storing the change history this.SetProperty(GRAPH_OBJ_PROP_BASE_NAME,0,this.Name()); // Base object name //--- Save the properties inherent in all graphical objects and present in a graphical object this.PropertiesRefresh(); //--- Save basic properties in the parent object this.m_create_time=(datetime)this.GetProperty(GRAPH_OBJ_PROP_CREATETIME,0); this.m_back=(bool)this.GetProperty(GRAPH_OBJ_PROP_BACK,0); this.m_selected=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTED,0); this.m_selectable=(bool)this.GetProperty(GRAPH_OBJ_PROP_SELECTABLE,0); this.m_hidden=(bool)this.GetProperty(GRAPH_OBJ_PROP_HIDDEN,0); //--- Initialize the extended graphical object toolkit if(this.GraphElementType()==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) { datetime times[]; double prices[]; if(::ArrayResize(times,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_TIME_DATA); if(::ArrayResize(prices,this.Pivots())!=this.Pivots()) CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_ARR_RESIZE_PRICE_DATA); for(int i=0;i<this.Pivots();i++) { times[i]=this.Time(i); prices[i]=this.Price(i); } this.ExtToolkit.SetBaseObj(this.TypeGraphObject(),this.Name(),this.ChartID(),this.SubWindow(),this.Pivots(),CTRL_FORM_SIZE,this.XDistance(),this.YDistance(),times,prices); this.ExtToolkit.CreateAllControlPointForm(); this.SetFlagSelected(false,false); this.SetFlagSelectable(false,false); } //--- Save the current properties to the previous ones this.PropertiesCopyToPrevData(); } //+-------------------------------------------------------------------+
Al inicializar el objeto de instrumental, primero declaramos las matrices de las propiedades de tiempo y precio; luego cambiamos sus tamaños según el número de puntos de pivote del objeto gráfico y, en un ciclo, escribimos en estas propiedades los valores de precio y tiempo de los puntos de pivote del objeto correspondientes al índice del ciclo.
A continuación, llamamos al método de inicialización del objeto de instrumental y le transmitimos los parámetros necesarios del objeto gráfico y las matrices recién rellenadas de las propiedades de precio y tiempo. Después de la inicialización, llamamos al método para crear los objetos de formulario en los puntos de pivote del objeto gráfico y, al final, establecemos para el objeto gráfico el estado de objeto no seleccionado y prohibimos que se seleccione con el ratón.
En el método que comprueba los cambios en las propiedades del objeto, en el bloque de código que procesa el objeto gráfico estándar extendido, escribimos un bloque de código para desplazar los puntos de pivote (objetos de formulario) a las nuevas coordenadas de pantalla al cambiar la ubicación de los puntos de pivote del objeto gráfico estándar extendido:
//+------------------------------------------------------------------+ //| Check object property changes | //+------------------------------------------------------------------+ void CGStdGraphObj::PropertiesCheckChanged(void) { CGBaseObj::ClearEventsList(); bool changed=false; int begin=0, end=GRAPH_OBJ_PROP_INTEGER_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_INTEGER prop=(ENUM_GRAPH_OBJ_PROP_INTEGER)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j)) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } begin=end; end+=GRAPH_OBJ_PROP_DOUBLE_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_DOUBLE prop=(ENUM_GRAPH_OBJ_PROP_DOUBLE)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j)) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } begin=end; end+=GRAPH_OBJ_PROP_STRING_TOTAL; for(int i=begin; i<end; i++) { ENUM_GRAPH_OBJ_PROP_STRING prop=(ENUM_GRAPH_OBJ_PROP_STRING)i; if(!this.SupportProperty(prop)) continue; for(int j=0;j<Prop.CurrSize(prop);j++) { if(this.GetProperty(prop,j)!=this.GetPropertyPrev(prop,j) && prop!=GRAPH_OBJ_PROP_NAME) { changed=true; this.CreateAndAddNewEvent(GRAPH_OBJ_EVENT_CHANGE,this.ChartID(),prop,this.Name()); } } } if(changed) { for(int i=0;i<this.m_list_events.Total();i++) { CGBaseEvent *event=this.m_list_events.At(i); if(event==NULL) continue; ::EventChartCustom(::ChartID(),event.ID(),event.Lparam(),event.Dparam(),event.Sparam()); } if(this.AllowChangeHistory()) { int total=HistoryChangesTotal(); if(this.CreateNewChangeHistoryObj(total<1)) ::Print ( DFUN,CMessage::Text(MSG_GRAPH_STD_OBJ_SUCCESS_CREATE_SNAPSHOT)," #",(total==0 ? "0-1" : (string)total), ": ",this.HistoryChangedObjTimeChangedToString(total-1) ); } //--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- get the data object of its pivot points, CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) continue; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); } //--- Save the current properties as the previous ones this.PropertiesCopyToPrevData(); } } //+------------------------------------------------------------------+
Si movemos uno de los puntos de pivote de un objeto gráfico, o el objeto completo, las coordenadas de pantalla de sus puntos de pivote cambiarán. En consecuencia, necesitaremos desplazar también a las nuevas coordenadas de pantalla los objetos de formulario de la clase de instrumental para que encajen en su lugar. Por lo tanto, aquí primero transmitimos las nuevas coordenadas del objeto gráfico al objeto de instrumental (en un ciclo por el número de puntos de pivote para las coordenadas de precio/tiempo, y las coordenadas aparte en píxeles), y luego llamamos al manejador de eventos del objeto de instrumental, transmitiéndole el identificador del evento de cambio del gráfico. Esto hará que el manejador de eventos del objeto de instrumental recalcule las coordenadas de pantalla de todos los formularios y los desplace a la nueva ubicación según las nuevas coordenadas de precio y tiempo del objeto gráfico.
En el método que añade un objeto gráfico estándar subordinado a la lista, corregimos un error: el objeto gráfico subordinado añadido cambia sus propiedades, por lo tanto, las nuevas propiedades deben corregirse inmediatamente como las anteriores, para que no se generen nuevos eventos de cambio de estos objetos gráficos al clicar en él:
//+------------------------------------------------------------------+ //| Add a subordinate standard graphical object to the list | //+------------------------------------------------------------------+ bool CGStdGraphObj::AddDependentObj(CGStdGraphObj *obj) { //--- If the current object is not an extended one, inform of that and return 'false' if(this.TypeGraphElement()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) { CMessage::ToLog(MSG_GRAPH_OBJ_NOT_EXT_OBJ); return false; } //--- If failed to add the pointer to the passed object into the list, inform of that and return 'false' if(!this.m_list.Add(obj)) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_FAILED_ADD_DEP_EXT_OBJ_TO_LIST); return false; } //--- Object added to the list - set its number in the list, //--- name and ID of the current object as the base one, //--- set the flags of object availability and selection //--- and the graphical element type - standard extended graphical object obj.SetNumber(this.m_list.Total()-1); obj.SetBaseName(this.Name()); obj.SetBaseObjectID(this.ObjectID()); obj.SetFlagSelected(false,false); obj.SetFlagSelectable(false,false); obj.SetTypeElement(GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED); obj.PropertiesCopyToPrevData(); return true; } //+------------------------------------------------------------------+
Manejador de eventos del objeto gráfico estándar abstracto:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) return; if(id==CHARTEVENT_CHART_CHANGE) { if(ExtToolkit==NULL) return; for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); ExtToolkit.OnChartEvent(id,lparam,dparam,sparam); } } //+------------------------------------------------------------------+
Hasta ahora, el manejador procesa solo el evento de cambio del gráfico.
Si el objeto no es extendido, salimos del manejador. Si se detecta un evento de cambio del gráfico, comprobamos la validez del puntero al objeto de instrumental del objeto gráfico estándar extendido. Si no se ha creado el instrumental, salimos. A continuación, en un ciclo por el número de puntos de pivote del objeto gráfico, establecemos las nuevas coordenadas de precio/tiempo del objeto gráfico en el objeto de instrumental. A continuación, establecemos sus nuevas coordenadas de pantalla y llamamos al manejador de eventos del objeto de instrumental, en el cual, al ocurrir un evento de cambio del gráfico, todos los formularios se establecen en las nuevas coordenadas de pantalla calculadas a partir de las nuevas coordenadas de percio/tiempo que se acaban de transmitir al objeto de instrumental.
Al eliminar un objeto gráfico estándar extendido de un gráfico, deberemos eliminar los objetos de formulario de su objeto de instrumental del gráfico, si dicho objeto se ha creado para el objeto gráfico. La eliminación de objetos gráficos de un gráfico la gestionaremos en la clase de colección de elementos gráficos, en el archivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
En el método que procesa la eliminación de objetos gráficos extendidos, escribimos un bloque de código para eliminar todos los objetos de formulario del objeto de instrumental:
//+------------------------------------------------------------------+ //| Handle the removal of extended graphical objects | //+------------------------------------------------------------------+ void CGraphElementsCollection::DeleteExtendedObj(CGStdGraphObj *obj) { if(obj==NULL) return; //--- Save the ID of the graphical object chart and the number of subordinate objects in its list long chart_id=obj.ChartID(); int total=obj.GetNumDependentObj(); //--- If the list of subordinate objects is not empty (this is the base object) if(total>0) { CGStdGraphObjExtToolkit *toolkit=obj.GetExtToolkit(); if(toolkit!=NULL) { toolkit.DeleteAllControlPointForm(); } //--- In the loop, move along all dependent objects and remove them for(int n=total-1;n>WRONG_VALUE;n--) { //--- Get the next graphical object CGStdGraphObj *dep=obj.GetDependentObj(n); if(dep==NULL) continue; //--- If failed to remove it from the chart, display the appropriate message in the journal if(!::ObjectDelete(dep.ChartID(),dep.Name())) CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); } //--- Upon the loop completion, update the chart to display the changes and exit the method ::ChartRedraw(chart_id); return; } //--- If this is a subordinate object else if(obj.BaseObjectID()>0) { //--- Get the base object name and its ID string base_name=obj.BaseName(); long base_id=obj.BaseObjectID(); //--- Get the base object from the graphical object collection list CGStdGraphObj *base=GetStdGraphObject(base_name,chart_id); if(base==NULL) return; //--- get the number of dependent objects in its list int count=base.GetNumDependentObj(); //--- In the loop, move along all its dependent objects and remove them for(int n=count-1;n>WRONG_VALUE;n--) { //--- Get the next graphical object CGStdGraphObj *dep=base.GetDependentObj(n); //--- If failed to get the pointer or the object has already been removed from the chart, move on to the next one if(dep==NULL || !this.IsPresentGraphObjOnChart(dep.ChartID(),dep.Name())) continue; //--- If failed to delete the graphical object from the chart, //--- display the appropriate message in the journal and move on to the next one if(!::ObjectDelete(dep.ChartID(),dep.Name())) { CMessage::ToLog(DFUN+dep.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); continue; } } //--- Remove the base object from the chart and from the list if(!::ObjectDelete(base.ChartID(),base.Name())) CMessage::ToLog(DFUN+base.Name()+": ",MSG_GRAPH_OBJ_FAILED_DELETE_OBJ_FROM_CHART); } //--- Update the chart for displaying the changes ::ChartRedraw(chart_id); } //+------------------------------------------------------------------+
Aquí, todo es sencillo: obtenemos del objeto el puntero a su objeto de instrumental del objeto gráfico extendido y, si el puntero es válido, llamamos al método para eliminar todos los formularios creados del objeto de instrumental del objeto gráfico estándar extendido que hemos analizado anteriormente.
En el manejador de eventos de la clase de colección de elementos gráficos, añadimos el procesamiento del cambio del gráfico para los objetos gráficos estándar extendidos:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj=NULL; ushort idx=ushort(id-CHARTEVENT_CUSTOM); if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { //--- Calculate the chart ID //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID //--- If the event ID corresponds to a user event, the chart ID is received from lparam //--- Otherwise, the chart ID is assigned to -1 long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE); long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param); //--- Get the object, whose properties were changed or which was relocated, //--- from the collection list by its name set in sparam obj=this.GetStdGraphObject(sparam,chart_id); //--- If failed to get the object by its name, it is not on the list, //--- which means its name has been changed if(obj==NULL) { //--- Let's search the list for the object that is not on the chart obj=this.FindMissingObj(chart_id); //--- If failed to find the object here as well, exit if(obj==NULL) return; //--- Get the name of the renamed graphical object on the chart, which is not in the collection list string name_new=this.FindExtraObj(chart_id); //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart, //--- and send an event with the new name of the object to the control program chart if(obj.SetNamePrev(obj.Name()) && obj.SetName(name_new)) ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj.ChartID(),obj.TimeCreate(),obj.Name()); } //--- Update the properties of the obtained object //--- and check their change obj.PropertiesRefresh(); obj.PropertiesCheckChanged(); } //--- Handle chart changes for extended standard objects if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE) { CArrayObj *list=this.GetListStdGraphObjectExt(); if(list!=NULL) { for(int i=0;i<list.Total();i++) { obj=list.At(i); if(obj==NULL) continue; obj.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } } } } //+------------------------------------------------------------------+
Aquí, si se ha registrado un evento de cambio del gráfico, obtenemos una lista con todos los objetos gráficos estándar extendidos, y en un ciclo por su número, obtenemos el siguiente objeto y llamamos a su manejador de eventos, transmitiendo a este el valor de evento "Gráfico cambiado".
Simulación
Para la prueba, tomaremos el asesor del artículo anteriory lo guardaremos en la nueva carpeta \MQL5\Experts\TestDoEasy\ Part95\ con el nuevo nombre TestDoEasyPart95.mq5.
Lo único que ha cambiado son los nombres ligeramente distintos de los objetos de etiqueta de precio fijados al objeto de línea de tendencia básico. Simplemente, en el manejador de eventos del asesor experto, en el bloque de creación de objetos gráficos compuestos, hemos añadido el texto "Ext" a los nombres de los objetos subordinados para que los nombres se correspondan con el tipo de objetos gráficos extendidos:
if(id==CHARTEVENT_CLICK) { if(!IsCtrlKeyPressed()) return; //--- Get the chart click coordinates datetime time=0; double price=0; int sw=0; if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price)) { //--- Get the right point coordinates for a trend line datetime time2=iTime(Symbol(),PERIOD_CURRENT,1); double price2=iOpen(Symbol(),PERIOD_CURRENT,1); //--- Create the "Trend line" object string name_base="TrendLineExt"; engine.CreateLineTrend(name_base,0,true,time,price,time2,price2); //--- Get the object from the list of graphical objects by chart name and ID and pass its properties to the journal CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID()); //--- Create the "Left price label" object string name_dep="PriceLeftExt"; engine.CreatePriceLabelLeft(name_dep,0,false,time,price); //--- Get the object from the list of graphical objects by chart name and ID and CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID()); //--- add it to the list of graphical objects bound to the "Trend line" object obj.AddDependentObj(dep); //--- Set its pivot point by X and Y axis to the trend line left point dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0); //--- Create the "Right price label" object name_dep="PriceRightExt"; engine.CreatePriceLabelRight(name_dep,0,false,time2,price2); //--- Get the object from the list of graphical objects by chart name and ID and dep=engine.GraphGetStdGraphObject(name_dep,ChartID()); //--- add it to the list of graphical objects bound to the "Trend line" object obj.AddDependentObj(dep); //--- Set its pivot point by X and Y axis to the trend line right point dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1); } } engine.GetGraphicObjCollection().OnChartEvent(id,lparam,dparam,sparam);
¿Qué pondremos a prueba? Vamos a crear un objeto gráfico compuesto. Al crearlo, los objetos de formulario se establecerán en sus puntos de pivote.
Estos objetos de formulario tienen coordenadas en píxeles desde la esquina superior izquierda de la pantalla. En consecuencia, si desplazamos el gráfico, estas coordenadas de pantalla deberán recalcularse para que los objetos se coloquen en los puntos de pivote correspondientes del objeto gráfico, cosa que vamos a comprobar a continuación.
Compilamos el asesor y lo ejecutamos en el gráfico:
Bien, a ver qué tenemos. Vemos lo siguiente: los objetos se colocan en su lugar al cambiar el gráfico, pero se retrasan mucho.
Al eliminar un objeto gráfico, también se eliminan los objetos de formulario que le pertenecen.
¿Qué podemos hacer con semejante retraso? En principio, nunca vamos a necesitar ver su movimiento en vivo: estos formularios siempre estarán ocultos al desplazarse el gráfico (ahora se muestran para controlar la reacción al evento). Además, la propia línea del objeto gráfico se moverá cuando estos objetos de formulario sean desplazados con el ratón. Y cualquier interacción con los formularios se realizará sobre un gráfico fijo. Por lo tanto, este resultado puede ser totalmente aceptable, sobre todo teniendo en cuenta que la actualización del gráfico no se realiza en cada iteración del ciclo, sino solo al final del mismo. No obstante, para aliviar la carga, podemos controlar la finalización del cambio del gráfico, y solo entonces representar los cambios y mostrar el objeto (y solo si el cursor se encuentra en el área activa del objeto de formulario, cuando debería ser visible).
¿Qué es lo próximo?
En el próximo artículo, continuaremos trabajando con los eventos de los objetos gráficos compuestos.
*Artículos de esta serie:
Gráficos en la biblioteca DoEasy (Parte 93): Preparando la funcionalidad para crear objetos gráficos compuestos
Gráficos en la biblioteca DoEasy (Parte 94): Objetos gráficos compuestos, desplazamiento y eliminación
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/10387
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso