
Soluciones sencillas para trabajar cómodamente con indicadores
Los indicadores forman desde hace tiempo parte integrante de cualquier plataforma comercial, casi todos los tráders los utilizan. Con frecuencia no se utiliza solo un indicador en el gráfico, sino todo el sistema, por lo que la comodidad de la configuración de los indicadores supone un aspecto importante en el trading.
En este artículo le contaremos cómo crear un panel simple para cambiar la configuración del indicador directamente desde el gráfico, y qué cambios se deberán introducir en el indicador para conectar este panel. Este artículo está dirigido exclusivamente a aquellos que acaban de empezar a familiarizarse con el lenguaje MQL5, por lo que hemos intentado explicar cada línea del código. Para los profesionales, si deciden leer el artículo, les podemos decir que no encontrarán nada nuevo aquí.
Construyendo el panel
Existen muchos paneles diferentes descritos en este sitio web, además de artículos y del código de CodeBase, ¿por qué no tener el código ya listo para usar? Hay mucho código de calidad por ahí, incluyendo grandes bibliotecas que nos permiten crear un panel de cualquier complejidad.
Sin embargo, no me gusta que la facilidad de uso de estas bibliotecas se sacrifique por una mayor universalidad. Por ello, hoy escribiremos un panel específico para indicadores; en la práctica, será una tabla normal, en la que la anchura y la altura de las celdas se ajustarán automáticamente a la anchura y la altura del texto en función del tamaño de la fuente.
Asimismo, tendremos una fila en la parte superior del panel que nos permitirá arrastrar el mismo. Esta fila contendrá el nombre del indicador, así como iconos para fijar y minimizar el panel. Cada celda de la tabla se describirá con una línea de código. De esta manera, podremos hacer que las columnas de diferentes filas tengan diferentes anchuras si fuera necesario.
Nombre del panel (indicador) "Fijar" "Minimizar" |
---|
Nombre de la configuración 1 | Campo de entrada |
Nombre del entorno 2 | Campo de entrada |
El código que describe la celda contendrá: el nombre del objeto, el tipo de objeto, el número de fila, el texto de la celda y la anchura de la celda en % de la anchura del panel. La anchura del panel y la anchura de la celda serán interdependientes. La suma de los porcentajes de todas las celdas de la misma fila deberá ser igual a 100%.
Supongamos que deseamos especificar tres objetos en una fila, entonces el número de fila de los tres objetos será el mismo, mientras que la anchura, por ejemplo, será 30% + 30% + 40% = 100%. En la mayoría de los casos, bastará con dividir la fila en dos partes: El 50% para el nombre del parámetro de configuración y el 50% bajo el campo de entrada.
Como he dicho, el código del panel se ha diseñado para que sea lo más sencillo posible. En principio, planeaba prescindir de la programación orientada a objetos (POO). pero no ha sido posible abandonarla por completo. Copiar mucho código de un indicador a otro resultaría muy incómodo. Así que hemos formalizado el código del panel como una clase en un archivo de inclusión.
He utilizado una clase en lugar de las funciones separadas habituales principalmente porque resulta cómodo eliminar los objetos del panel en el destructor, de lo contrario habría tenido que realizar la eliminación en el OnDeinit() del indicador, cosa que habría sido más difícil.
También tendremos un archivo de inclusión Object.mqh con métodos para dibujar objetos, y también hemos escrito getters y setters en él para facilitar el acceso a las funciones. No voy a describir lo que son los getters y setters. Los que aún no lo conozcan pueden recurrir a Yándex: esta herramienta se lo contará mejor que yo.
He tomado prestada parte de la idea del panel de estos artículos: artículo 1, artículo 2.
Todos los archivos de código descritos aquí se adjuntan al final del artículo. Le recomiendo descargarlos, colocarlos en carpetas y solo entonces empezar a estudiar el código. Yo tengo una carpeta Object aparte creada para el archivo Object.mqh en la carpeta include. Para el archivo Panel.mqh se crea una carpeta independiente Panel en la carpeta include. Como consecuencia, la ruta a estos archivos en mi código se especifica considerando las carpetas anidadas.
Comenzaremos conectando el archivo de inclusión Object.mqh y declarando las variables de entrada. Necesitamos declarar las variables en las que especificaremos: los colores del panel, el texto, los botones y los marcos, así como los colores adicionales en los que se coloreará el panel cuando el indicador esté oculto, el tamaño de fuente, el estilo de fuente y la separación del panel desde los bordes del gráfico.
Ajustes de entrada:
//+------------------------------------------------------------------+ #include <Object\\Object.mqh> //+------------------------------------------------------------------+ input group "--- Input Panel ---" input int shiftX = 3; // Panel offset along the X axis input int shiftY = 80; // Panel offset along the Y axis input bool NoPanel = false; // No panel input int fontSize = 9; // Font size input string fontType = "Arial"; /* Font style*/ //"Arial", "Consolas" input string PanelHiddenShown = "❐"; // Panel hidden/displayed input string PanelPin = "∇"; /* Pin the panel*/ // ⮂ ↕ ↔ ➽ 🖈 ∇ input string PanelUnpin = "_"; // Unpin the panel input color clrTitleBar = C'109,117,171'; // Panel title background color (1) input color clrTitleBar2 = clrGray; // Panel title background color (2) input color clrDashboard = clrDarkGray; // Panel background color input color clrTextDashboard = clrWhite; // Text color on the panel input color clrBorder = clrDarkGray; // Border color input color clrButton1 = C'143,143,171'; // Button background color (1) input color clrButton2 = C'213,155,156'; // Button background color (2) input color clrButton3 = clrGray; // Button background color (3) input color clrTextButton1 = clrBlack; // Button text color (1) input color clrTextButton2 = clrWhite; // Button text color (2) input color clrEdit1 = C'240,240,245'; // Input field background color (1) input color clrEdit2 = clrGray; // Input field background color (2) input color clrTextEdit1 = C'50,50,50'; // Input field text color (1) input color clrTextEdit2 = clrWhite; // Input field text color (2) //+------------------------------------------------------------------+
Lo siguiente será la propia clase CPanel:
//+------------------------------------------------------------------+ class CPanel { private: enum ENUM_FLAG //flags { FLAG_PANEL_HIDDEN = 1, // panel hidden FLAG_PANEL_SHOWN = 2, // panel displayed FLAG_IND_HIDDEN = 4, // indicator hidden FLAG_IND_SHOWN = 8, // indicator displayed FLAG_PANEL_FIX = 16, // panel pinned FLAG_PANEL_UNPIN = 32 // panel unpinned }; int sizeObject; int widthPanel, heightPanel; int widthLetter, row_height; int _shiftX, _shiftY; long mouseX, mouseY; long chartWidth, chartHeight; string previousMouseState; long mlbDownX, mlbDownY, XDistance, YDistance; string _PanelHiddenShown, _PanelPin, _PanelUnpin; struct Object { string name; string text; ENUM_OBJECT object; int line; int percent; int column; int border; color txtColr; color backClr; color borderClr; }; Object mObject[]; int prefixInd; string Chart_ID; string addedNames[]; long addedXDisDiffrence[], addedYDisDiffrence[]; int WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL); void Add(string name); // save the object name and anchor point void HideShow(bool hide = false); // hide//show void DestroyPanel(); // delete all objects public: CPanel(void); ~CPanel(void); string namePanel; // panel name string indName; // indicator name should match indicator short name string prefix; // prefix for panel object names bool hideObject; // To be used as a flag in indicators where graphical objects need to be hidden int sizeArr; double saveBuffer[]; // array for storing the coordinates of the panel anchor point, panel properties (flag states), and the latest indicator settings enum ENUM_BUTON // flags for allowing button creation { BUTON_1 = 1, BUTON_2 = 2 }; void Init(string name, string indName); void Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);}; void Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0); bool OnEvent(int id, long lparam, double dparam, string sparam); int Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();} bool Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);} void Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1); void ApplySaved(); void HideShowInd(bool hide); }; //+------------------------------------------------------------------+ CPanel::CPanel(void) {} //+------------------------------------------------------------------+ CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();} //+------------------------------------------------------------------+
Luego hablaremos de los métodos de la clase con ejemplos.
Por ejemplo, escribiremos un indicador vacío:
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param; //+------------------------------------------------------------------+ int OnInit() { string short_name = "Ind Pnl(" + (string)param + ")"; mPanel.Init("Ind Pnl", short_name); mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60); mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40); mPanel.Create(0); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
Al ejecutar este indicador, obtendremos este panel en el gráfico:
Y ahora, utilizando este indicador como ejemplo, analizaremos con detalle el código del panel.
Justo después del indicador de input de parámetros, conectaremos el archivo con la clase panel y declararemos la clase de panel.
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param;
Normalmente, la clase se declarará al principio del código. Pero como la clase panel está en un archivo de inclusión que también tiene parámetros de entrada, si la escribimos al principio del código de entrada, los parámetros del indicador quedarán por debajo de los parámetros input del panel. Y esto nos creará algunos inconvenientes a la hora de iniciar y configurar el indicador.
Como las variables de entrada son constantes, no pueden modificarse. Pero podemos crear una copia de las variables input y más tarde trabajar con ellas y cambiarlas desde el campo de entrada del panel.
A continuación, en la función de indicador OnInit(), añadiremos el código del panel.
Pero primero querría llamar su atención sobre el siguiente hecho: para que el panel funcione correctamente, deberemos establecer para el indicador un nombre de indicador corto en el código en el que los principales parámetros input quedarán indicados.
string short_name = "Ind Pnl(" + (string)_param + ")";
Esto es necesario para ejecutar el indicador con diferentes configuraciones.
Le recuerdo que algunos símbolos no pueden usarse en los nombres de los indicadores. Si desea separar los parámetros con dos puntos, será mejor sustituirlos por un punto y coma.
El nombre del panel puede ser el mismo que el del indicador, pero resultará más cómodo crear el nombre del panel sin considerar los parámetros del indicador.
El primer método de la clase CPanel, que añadiremos al indicador, será el método Init() al que transmitiremos dos nombres: el nombre del panel y el nombre del indicador.
mPanel.Init("Ind Pnl", short_name);
Y lo primero que hace el método Init() es comprobar si el panel está desactivado en la configuración.
void CPanel::Init(string name, string short_name) { if(NoPanel) return;
A continuación, inicializaremos las variables:
namePanel = name; indName = short_name; MovePanel = true; sizeObject = 0; Chart_ID = (string)ChartID(); int lastX = 0, lastY = 0;
Luego estableceremos el permiso para enviar mensajes a todos los programas mql5 en el gráfico sobre los eventos de desplazamiento y pulsación del botón del ratón (CHARTEVENT_MOUSE_MOVE), así como el permiso del envío de mensajes sobre el evento de creación de un objeto gráfico (CHARTEVENT_OBJECT_CREATE):
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
Para calcular la anchura del panel, primero deberemos establecer el tipo de fuente y su tamaño, y también obtener el tamaño de un carácter, para poder calcular la separación del texto a partir de los bordes del panel.
Así es como obtendremos separaciones escalables vinculadas al tamaño de la fuente. La altura de la celda será igual a la altura de un carácter y medio.
// set the font type and size TextSetFont(fontType, fontSize * -10); // get the width and height of one character TextGetSize("0", widthLetter, row_height); // calculate the cell height row_height += (int)(row_height / 2);
En los ajustes del panel, hay iconos que sirven para ocultar/mostrar el panel ❐, y para anclarlo/desanclarlo ∇ y _.
He encontrado los iconos en Internet y se pueden cambiar en la configuración.
Ahora añadiremos los espacios a los caracteres para posicionarlos correctamente desde el borde del panel. Si no añadimos espacios, los iconos se mostrarán demasiado cerca unos de otros y resultará problemático incidir en ellos con el ratón.
string space = " "; _PanelHiddenShown = space + PanelHiddenShown + space; _PanelPin = space + PanelPin + space; _PanelUnpin = space + PanelUnpin + space;
El panel se compone de objetos gráficos, así que para que los nombres de estos objetos sean únicos, crearemos un prefijo para ellos:
MathSrand((int)GetMicrosecondCount()); prefixInd = MathRand(); prefix = (string)prefixInd;
Me gustaría llamar su atención sobre un hecho importante: si hay varios indicadores con un panel en el gráfico, no será posible usar la función GetTickCount() al crear un prefijo, porque al cambiar de marco temporal, pasará tan poco tiempo que si utilizamos milisegundos en lugar de microsegundos, los prefijos de algunos paneles podrían coincidir.
Al arrastrar el panel, se utilizará la función OnChartEvent(), que define la posición del ratón en el gráfico y en el objeto. Y es que un panel puede chocar con otro, y entonces los paneles entrarán un conflicto, pues ambos pensarán que el ratón está arrastrando ese panel en particular. Como resultado, se arrastrarán todos los paneles que estén bajo el ratón. Para evitar conflictos, crearemos una variable global y, cuando el ratón haga clic en un panel, el panel que primero escriba su prefijo en esta variable global desplazará el ratón. El primero que levante la mano, se lleva el premio.
En la inicialización escribiremos cero en esta variable; mientras se escriba cero en la variable, se considerará libre.
GlobalVariableTemp("CPanel"); GlobalVariableSet("CPanel", 0);
Cuando movemos, minimizamos, fijamos el panel o cambiamos los parámetros del indicador, necesitamos guardar estos cambios en algún sitio: esto nos permitirá cargar el panel y el indicador con la última configuración al cambiar de marco temporal o reiniciar el terminal. En el indicador presentado no hemos incluido el código para guardar los últimos ajustes del panel y del indicador, pero incluso sin este código el indicador registrará los cambios de los ajustes del panel. Para ello, deberemos asignar memoria a un array con la configuración del panel.
sizeArr = ArraySize(saveBuffer); if(sizeArr == 0) Resize(0);
Aunque transmitamos a la función el número de ajustes del indicador = 0, Resize(0); se añadirán tres celdas en la propia función para recordar los ajustes del panel. Es decir, para recordar la posición del panel en el gráfico, su estado (fijo/no fijo, minimizado/desplegado) y también el estado del indicador (mostrado/oculto), se utilizan tres celdas del array saveBuffer.
A continuación encontramos el código que define las coordenadas iniciales del punto de anclaje del panel. La cuestión es que el punto de anclaje del panel puede tomarse de los ajustes de entrada, o bien de los almacenados, si el panel ya se ha dibujado en este gráfico. Existe otra opción: si utilizamos la plantilla en la que se ha escrito el indicador con el panel.
La plantilla ha resultado más complicada. Si guardamos una plantilla que tiene un indicador con un panel, no habrá forma de guardar las coordenadas donde estaba el panel cuando se creó la plantilla.
Pero si añadimos un indicador al gráfico, guardamos la plantilla y luego aplicamos esta plantilla, veremos que el objeto de etiqueta de texto OBJ_LABEL se ha escrito en la plantilla.
Plantilla guardada:
Plantilla aplicada:
Precisamente estas etiquetas de texto son las que utilizaremos para determinar la posición del panel al momento de crear la plantilla.
string delPrefix = ""; int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL); for(int i = 0; i < total; i++) { string nameObject = ObjectName(0, i, 0, OBJ_LABEL); if(StringFind(nameObject, "TitleText " + indName) >= 0) // if the template contains objects with the name of this indicator { lastX = (int)GetXDistance(nameObject); // define the X coordinates of the panel in the template lastY = (int)GetYDistance(nameObject); // define the Y coordinates of the panel in the template StringReplace(nameObject, "TitleText " + indName, ""); // remember the object prefix for its subsequent deletion delPrefix = nameObject; } }
Las variables lastX y lastY almacenan las coordenadas del punto de anclaje del objeto: una etiqueta de texto con un nombre que contiene el nombre del indicador después del prefijo, es decir, las coordenadas del texto con el nombre del panel.
Recuerde que el nombre del panel puede ser diferente del nombre del indicador. Una vez hayamos encontrado el texto deseado, seleccionaremos el prefijo del mismo y lo guardaremos.
El siguiente código, usando el prefijo guardado en el paso anterior, eliminaremos del gráfico las etiquetas de texto «artefactuales» que se han almacenado en la plantilla.
if(delPrefix != "")// delete obsolete objects saved in the template ObjectsDeleteAll(0, delPrefix);
A continuación, comprobaremos y seleccionaremos la variante deseada del punto de anclaje del panel.
if(lastX != 0 || lastY != 0)// if we use a template { lastX = lastX - widthLetter / 2; lastY = lastY - (int)(row_height / 8); saveBuffer[sizeArr - 1] = _shiftX = lastX; saveBuffer[sizeArr - 2] = _shiftY = lastY; } else// if data from the file is used if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0) { _shiftX = (int)saveBuffer[sizeArr - 1]; _shiftY = (int)saveBuffer[sizeArr - 2]; } else// if this is the first launch of the indicator { saveBuffer[sizeArr - 1] = _shiftX = shiftX; saveBuffer[sizeArr - 2] = _shiftY = shiftY; }
Al final del método Init() enviaremos al array de estructuras aquellos objetos panel que no necesiten ser editados; serán los mismos para todos los paneles.
Dos rectángulos, texto con el nombre del panel e iconos para ocultar/mostrar y para anclar/desanclar el panel.
Record("TitleBar"); Record("MainDashboardBody"); Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100); Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0); Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);
Ya hemos analizado el método Init(), pasemos ahora al siguiente método, Record().
El método Record() rellena la estructura de un objeto futuro. Normalmente, la mayor parte de la estructura se rellena con valores por defecto, pero el conjunto de parámetros que se transmiten a esta función permite modificar ligeramente los valores por defecto y, por ejemplo, establecer un color diferente para el objeto.
//+------------------------------------------------------------------+ void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0) { if(NoPanel) return; int column = WidthHidthCalc(line + 1, text, percent, object); ArrayResize(mObject, sizeObject + 1); mObject[sizeObject].column = column; // column mObject[sizeObject].name = prefix + name; // object name mObject[sizeObject].object = object; // object type mObject[sizeObject].line = line + 1; // line index mObject[sizeObject].text = text; // text (if any) mObject[sizeObject].percent = percent; // percentage of panel width mObject[sizeObject].txtColr = txtColr; // text color mObject[sizeObject].backClr = backClr; // base color mObject[sizeObject].borderClr = borderClr; // border color mObject[sizeObject].border = 0; // offset from the panel edge sizeObject++; } //+------------------------------------------------------------------+
Al principio del método Record() hay una llamada al método WidthHidthCalc(), que calcula la anchura del panel y su altura.
Vamos a estudiar con detalle el método WidthHidthCalc().
En este método, la anchura del panel se calculará considerando el elemento de más anchura. Por ejemplo, si cambiamos el nombre del indicador "Ind Pnl" anterior por un nombre más largo.
Antes era:
mPanel.Init("Ind Pnl", short_name);
Ahora es:
mPanel.Init("Ind Pnl 0000000000000000000", short_name);
Tendremos un panel como este:
O si, por ejemplo, cambiamos el nombre de la configuración del indicador, obtendremos lo siguiente.
Antes era:
mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
Ahora es:
mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);
Resultado:
El propio panel se ajusta al tamaño del texto. Todos los cálculos de anchura y altura del panel se han realizado en la función WidthHidthCalc().
En primer lugar, obtendremos la anchura del texto de la celda.
La anchura para el texto con el nombre del panel y los iconos ocultar/mostrar se encuentra de una forma ligeramente diferente de las otras celdas.
int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL) { static int lastLine = -1, column = 0; int width, height; if(line == 1) TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); // get the width and height of the text for the line with the panel name else TextGetSize(text, width, height); // get the text width and height
El texto debe estar separado desde el borde de la celda, y haremos que esta separación sea igual a medio carácter. Ya hemos encontrado la anchura de un carácter en Init() y la hemos escrito en la variable widthLetter.
Para posibilitar la separación del texto en ambos lados, necesitaremos añadir la anchura de un carácter adicional a la anchura de texto obtenida, y para el texto en el objeto "Botón" - OBJ_BUTTON, necesitaremos añadir un carácter adicional para la separación de los bordes del botón.
Y tras conocer el tamaño de toda la fila de la celda, teniendo en cuenta las separaciones, podremos calcular qué tamaño tendrá el panel, considerando los porcentajes especificados para la celda.
Ahora guardaremos el valor más alto del panel. A partir de ahora, todas las celdas se calcularán según este mayor valor de la anchura del panel.
double indent = 0; if(object == OBJ_BUTTON) indent += widthLetter; if(text != "" && percent != 0) { // calculate the width of the panel based on the text size and the percentage allocated for this text int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent); if(widthPanel < tempWidth) widthPanel = tempWidth; }
El cálculo de la anchura del panel en el indicador de prueba tendrá el aspecto que sigue.
En primer lugar, se calculará la anchura del nombre, incluidos los iconos. El nombre "Ind Pnl" + " ∇ " + " ❐ " ha resultado tener una anchura de 71px más la anchura de un carácter es 7px, para un total de 78px, es decir, el 100% de la anchura del panel.
El texto de la celda es "param"; la anchura supone 36 px con separaciones añadidas de 7 px, así que el resultado será de 43 px; el 60% de la anchura del panel se asignará a esta celda, por lo que la anchura del panel será 43 * 100 / 60 = 72 px. Esto es menos de lo que necesitamos para el nombre del panel, por lo que la anchura del panel será igual a la celda con el nombre.
A continuación, determinaremos el número de columna y/o añadiremos la altura del panel si se trata de una nueva fila.
if(lastLine != line) // if this is a new row in the panel, then increase the height of the entire panel { heightPanel = row_height * line; lastLine = line; column = 0; // reset the number of columns in the new row } else column++; // add a new column return column; }
Aquí hemos analizado con detalle el funcionamiento de dos de los diez métodos de la clase CPanel.
Después de que el programa haya definido las futuras dimensiones del panel y escrito los parámetros del objeto en el array de estructuras mObject[], pasaremos al siguiente método: Create(). Este método construirá el panel basándose en las dimensiones obtenidas previamente.
Al principio del método, como de costumbre, comprobaremos si este panel es necesario. A esto le seguirá un código para escribir dos botones predefinidos. Uno para ocultar el indicador y otro para eliminarlo. Según los indicadores seleccionados, podremos elegir 0-ningún botón, 1-botón para ocultar/mostrar el indicador, 2-botón para borrar el indicador, 3-se crearán ambos botones.
¿Y por qué estos botones están aquí y no en el código del indicador? Solo para cargar con menos código el código de los indicadores.
El siguiente paso consistirá en inicializar las variables. Y este código será necesario si necesitamos utilizar el panel no exactamente como se pretende, sino, por ejemplo, como un panel emergente para cambiar los parámetros de los objetos, y necesitamos que el panel aparezca en el lugar del clic del ratón en el gráfico.
void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1) { if(NoPanel) return; if((Button & BUTON_1) == BUTON_1) // if we need to create buttons Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50); if((Button & BUTON_2) == BUTON_2) // if we need to create buttons Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2); ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER; int X = 0, Y = 0, xSize = 0, ySize = 0; if(shiftx != -1 && shifty != -1) { _shiftX = shiftx; _shiftY = shifty; }
Explicación a "y este código ...": el panel se puede adaptar a los paneles de información y comercio, y a este panel podemos adaptar la configuración de objetos gráficos. Todo es sencillo, sin florituras ni arabescos, solo funcionalidad:
He divagado un poco: continuemos analizando el método Create(). A continuación verá un código que crea dos rectángulos, el rectángulo de encabezado y el rectángulo del cuerpo del panel:
// header rectangle RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName); Add(mObject[0].name);// remember the object's anchor point // panel rectangle RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName); Add(mObject[1].name);
Después de crear cada objeto, se llamará a la función Add(), que escribe en los arrays el nombre y las coordenadas de este objeto relativas a la esquina superior izquierda del gráfico.
//+------------------------------------------------------------------+ void CPanel::Add(string name)// save the object name and anchor point { int size = ArraySize(addedNames); ArrayResize(addedNames, size + 1); ArrayResize(addedXDisDiffrence, size + 1); ArrayResize(addedYDisDiffrence, size + 1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name); } //+------------------------------------------------------------------+
Más tarde estos arrays con coordenadas se utilizarán al desplazar el panel.
Volvamos de nuevo al código del método Create(). Además, todos los objetos se crean en un ciclo en la secuencia en la que se han escrito en el array de estructuras mObject[]. Primero calcularemos las coordenadas y las dimensiones y, a continuación, crearemos el objeto.
Como el panel es muy especializado, solo usará tres tipos de objetos, y esto bastará para su funcionalidad.
Al rellenar el rectángulo de cabecera con texto, hemos tenido que recurrir a excepciones y hacer que el punto de anclaje de los iconos de fijación y minimización del panel fuera distinto del punto de anclaje de todos los demás objetos del panel. Esto ha facilitado la colocación de estos iconos en el panel, ya que tienen un punto de anclaje en la esquina superior derecha.
for(int i = 2; i < sizeObject; i++) { // calculate the coordinates of the object anchor point if(mObject[i].column != 0) { X = mObject[i - 1].border + widthLetter / 2; mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100); } else { X = _shiftX + widthLetter / 2; mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100); } Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8); //--- switch(mObject[i].object) { case OBJ_LABEL: ap = ANCHOR_LEFT_UPPER; // unlike all other objects, the "pin" and "collapse" objects' anchor points are implemented in the upper right corner. if(i == 3) { int w, h; TextGetSize(_PanelHiddenShown, w, h); X = _shiftX + widthPanel - w; ap = ANCHOR_RIGHT_UPPER; } if(i == 4) { X = _shiftX + widthPanel; ap = ANCHOR_RIGHT_UPPER; } LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1); break; case OBJ_EDIT: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER, (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1); break; case OBJ_BUTTON: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1); break; } Add(mObject[i].name); }
Una vez construidos todos los objetos del panel, eliminaremos el array de estructuras mObject[], ya que no será necesario.
ArrayFree(mObject); ApplySaved(); ChartRedraw();
Suelo crear una función aparte si el código se usa más de una vez. Sin embargo, si la esencia de la operación se agrupa según el significado, la asigno a un método independiente. Por ejemplo, esto es lo que hemos hecho con ApplySaved(). Comprobamos si ya hay datos del panel guardados y los aplicamos si los hay, o guardamos datos nuevos si no los hay.
ApplySaved()
Si es la primera vez que se ejecuta el indicador en este gráfico, el array saveBuffer[] se rellenará con la configuración inicial.
Si en el array saveBuffer[] ya hay datos guardados, los aplicaremos en lugar de la configuración inicial.
//+------------------------------------------------------------------+ void CPanel::ApplySaved() { // collapse the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN) CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN; // hide the indicator immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN) { HideShowInd(true); SetButtonState(prefix + "hideButton", true); hideObject = true; } else { saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN; hideObject = false; } // pin the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX) SetText(addedNames[3], _PanelUnpin); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN; int Err = Save(); if(Err != 0) Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName); } //+------------------------------------------------------------------+
Como probablemente habrá notado, en la función ApplySaved() también se encuentran las funciones Save(), HideShowInd() y OnEvent(); si está leyendo esto, por favor escriba la palabra "notado" en el comentario, es muy interesante saber si alguien lee estas descripciones o no.
Ahora pasaremos a la descripción de estas funciones por orden. En Save() guardaremos la configuración obtenida. Para no saturar la carpeta Files, asignaremos una carpeta pnl separada para los ajustes guardados del panel
Este será el aspecto de la función Save():
int Save() { ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError(); }
HideShowInd()
El objetivo de esta función consistirá simplemente en cambiar el color del encabezado del panel y el color y texto del botón. Del array saveBuffer borraremos la bandera anterior y escribiremos la nueva.
La variable hideObject solo será necesaria en aquellos indicadores en los que se utilicen objetos (flechas, iconos, texto, etc.) para el dibujado. Al crear un nuevo objeto en el indicador, comprobaremos el estado de esta variable y, dependiendo del estado, ocultaremos directamente los objetos recién creados o no haremos nada y los objetos se mostrarán.
//+------------------------------------------------------------------+ void CPanel::HideShowInd(bool hide) { // change the color and text of the buttons depending on the state of the panel, hidden/displayed, as well as the header color if(hide) { SetColorBack(prefix + "TitleBar", clrTitleBar2); SetColorBack(prefix + "hideButton", clrButton3); SetText(prefix + "hideButton", "Ind Show"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN; hideObject = true; } else { SetColorBack(prefix + "TitleBar", clrTitleBar); SetColorBack(prefix + "hideButton", clrButton1); SetText(prefix + "hideButton", "Ind Hide"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN; hideObject = false; } Save(); ChartRedraw(); } //+------------------------------------------------------------------+
Esta función solo se usará al pulsar el botón Ocultar/ la representación del indicador Ind Show/Ind Hide si se utiliza.
Ejemplo de ocultación de uno de los dos indicadores RSI
El código también contiene la función HideShow(), encargada de ocultar y mostrar objetos. Esta función se usa para contraer el panel y traer los objetos del panel al primer plano.
La función toma un argumento que indicará si el panel está plegado o no (true/false). Si el panel está plegado, solo habrá que traer al primer plano cuatro de sus objetos: el rectángulo con el nombre, el propio nombre y dos iconos: para fijar el panel y contraerlo.
Si la bandera es igual a true (es decir, si el panel está plegado), ocultaremos los cinco objetos secuencialmente y luego los mostraremos. ¿Por qué cinco y no cuatro? La cuestión es que entre los objetos necesarios hay uno superfluo: el rectángulo del propio panel. Lo creamos antes del título y los iconos, por lo que este rectángulo deberá ocultarse por separado.
Si la bandera es igual a false, todoslos objetos del panel se ocultarán secuencialmente y luego se mostrarán, trayéndolos así al primer plano.
//+------------------------------------------------------------------+ void CPanel::HideShow(bool hide = false) // hide and immediately display objects to bring to the foreground { int size = hide ? 5 : ArraySize(addedNames); for(int i = 0; i < size; i++) { SetHide(addedNames[i]); SetShow(addedNames[i]); } if(hide) SetHide(addedNames[1]); } //+------------------------------------------------------------------+
Este será el aspecto del panel plegado junto al panel normal:
La siguiente función que veremos es OnEvent().
Al principio de la función, comprobaremos si el panel está permitido en la configuración:
bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam) { if(NoPanel) return false;
A continuación, veremos el código responsable de desplazar el panel. Vamos a intentar explicar el funcionamiento del código con el mayor detalle posible.
Cuando se produce el evento "desplazamiento de ratón " y en la memoria se ha escrito la bandera que permite mover el panel, entonces memorizamos las coordenadas del ratón.
Si se trata de una pulsación del botón izquierdo, el valor del argumento sparam será 1, y si el ratón no ha sido pulsado antes, leeremos el valor de la variable global.
Esta variable global será común a todos los paneles que se ejecutarán en el terminal. Al desplazar un panel, comprobaremos si el valor del prefijo de otro panel está escrito en esta variable, y si no es así, el prefijo del panel se escribirá en esta variable global, y los otros paneles, si están debajo/sobre el panel que estamos moviendo, ya no se pueden desplazar.
Si en la variable global se ha escrito cero o el prefijo de este panel, a continuación, leeremos las coordenadas actuales del punto de anclaje del panel.
A continuación, comprobaremos si se ha clicado con el ratón en el rectángulo con el encabezado del panel, y si es así, obtendremos el tamaño del gráfico en píxeles, prohibiremos el desplazamiento (movimiento) del gráfico y escribiremos el prefijo del panel dado en la variable global. La entrada del prefijo será a la vez una bandera que permite desplazar el panel y garantiza que solo se moverá ese panel.
Por último, ocultamos/representamos el panel para traerlo al primer plano, al tiempo que transmitimos un argumento (true/false) a la función para saber si el panel está actualmente oculto, o si se representa al completo.
if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { mouseX = (long)lparam; mouseY = (long)dparam; if(previousMouseState != "1" && sparam == "1") { int gvg = (int)GlobalVariableGet("Panel"); if(gvg == prefixInd || gvg == 0) { XDistance = GetXDistance(addedNames[0]); YDistance = GetYDistance(addedNames[0]); mlbDownX = mouseX; mlbDownY = mouseY; if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height) { chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); ChartSetInteger(0, CHART_MOUSE_SCROLL, false); GlobalVariableSet("Panel", prefixInd); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // hide/display the panel so that it is in the foreground } } }
Después de que el usuario clique en el rectángulo con el nombre del panel y empiece a mover el ratón, se ejecutará el siguiente código.
En primer lugar, comprobaremos si se permite mover el panel. Si la variable global tiene un prefijo para ese panel, podremos desplazarlo. A continuación, buscaremos las coordenadas del punto de anclaje del panel. Si movemos el panel según las coordenadas del ratón, comprobaremos la nueva posición del panel. Si el panel puede salirse de los límites del gráfico, cambiaremos ligeramente los valores de las coordenadas del punto de anclaje para que el panel no sobrepase el gráfico.
En un ciclo, desplazaremos todos los objetos del panel.
Luego escribiremos las nuevas coordenadas del punto de anclaje del panel en un array para su posterior escritura en un archivo. Después, redibujaremos el gráfico.
if((int)GlobalVariableGet("Panel") == prefixInd) { // disable the ability to go beyond the chart for the panel long posX = XDistance + mouseX - mlbDownX; if(posX < 0) posX = 0; else if(posX + widthPanel > chartWidth) posX = chartWidth - widthPanel; long posY = YDistance + mouseY - mlbDownY; if(posY < 0) posY = 0; else if(posY + row_height > chartHeight) posY = chartHeight - row_height; // move the panel int size = ArraySize(addedNames); for(int i = 0; i < size; i++) { SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]); SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]); } saveBuffer[sizeArr - 1] = (double)(posX); saveBuffer[sizeArr - 2] = (double)(posY); ChartRedraw(0); }
La última acción de desplazamiento el panel sucederá al soltar el botón del ratón, el valor de sparam ya no será igual a uno.
Luego devolveremos la capacidad de desplazar el gráfico, restableceremos la variable global a cero y escribiremos las nuevas coordenadas del punto de anclaje del panel en el archivo.
if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd) { ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); } previousMouseState = sparam; }
Ya hemos detallado el mecanismo de arrastre de los paneles, así que ahora veremos las acciones que suceden al pulsar sobre los iconos de fijar/desfijar un panel o minimizar/desplegar el mismo.
Todo ello en la misma función OnEvent().
Cuando sucede un evento de pulsación del ratón en un objeto gráfico, en la variable sparam se encontrará el nombre del objeto en el que se ha dado el clic del ratón; si este coincide con el nombre del objeto "❐", comprobaremos el siguiente objeto, y si este es visible, ocultaremos los objetos del panel, y si no es visible, representaremos todos los objetos del panel. Luego cambiaremos el indicador de visibilidad del panel y lo escribiremos en un array para guardarlo posteriormente en un archivo.
Como el icono "❐" se encuentra en la zona por la que se arrastra el panel, el desplazamiento del gráfico suele estar desactivado, así que estableceremos el permiso de desplazamiento. Por la misma razón, también deberemos poner a cero la variable global.
Guardaremos los cambios en un archivo para visualizarlos en el gráfico y volver a dibujarlo.
else if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == addedNames[4]) // prefix+"CollapseExpand" { if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// if the panel is visible, hide it { SetHide(addedNames[1]); for(int i = 5; i < sizeObject; i++) SetHide(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN; } else// if the panel is hidden, display it { for(int i = 0; i < sizeObject; i++) SetShow(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN; } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
El siguiente código será similar al anterior, con la única diferencia del nombre del objeto "∇"
else if(sparam == addedNames[3]) // prefix+"PinUnpin" { if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX; SetText(addedNames[3], _PanelUnpin); } else { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN; SetText(addedNames[3], _PanelPin); } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
El código se finalizará con el botón de eliminación del indicador:
else if(sparam == prefix + "delButton") // handle the indicator deletion button ChartIndicatorDelete(0, ChartWindowFind(), indName); }
El último evento que tendremos que procesar es la creación de un objeto gráfico.
Los objetos recién creados suelen colocarse en primer plano y pueden superponerse al panel, por lo que primero guardaremos el estado de selección del objeto, después ocultaremos/representaremos el panel para situarlo en primer plano, y después restableceremos el estado de selección del objeto. ¿Por qué tomarse tantas molestias? El problema es que la ocultación de la visualización de los objetos del panel usando software provoca la deselección de los objetos recién creados.
Este fenómeno se describe con más detalle en este artículo.
else if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179 "Making a dashboard to display data in indicators and EAs" { bool select = GetSelect(sparam); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// hide/display the panel so that it is in the foreground SetSelect(sparam, select);// restore the state of the extreme object } return true; }
Aquí termina la descripción del código del panel.
¿Qué modificaciones deberemos introducir en el código del indicador?
Diferentes indicadores requieren diferentes cambios para indicadores distintos. No hemos podido crear un código universal que pueda insertarse en el indicador y obtener un panel de control listo para usar. Deberemos trabajar con cada indicador por separado.
En este artículo daremos algunos ejemplos para ayudarle a cambiar otros indicadores por analogía.
Por coherencia, hemos añadido la palabra "Pnl" a los nombres de los indicadores que han sufrido modificaciones.
Indicador personalizado de media móvil
Para controlar el indicador desde el panel, deberemos poder cambiar las variables de entrada. Sin embargo, las variables de entrada son constantes y no pueden modificarse.
Para resolver este problema, podemos copiar las variables de entrada en variables regulares que puedan modificarse. Para minimizar los cambios en el código del indicador, declararemos nuevas variables con los mismos nombres que las variables de entrada actuales, pero añadiéndoles un guión bajo al inicio.
Antes era:
Ahora es:
Justo después de los parámetros de entrada, vamos a conectar el archivo de inclusión Panel.mqh y declarar una instancia de la clase CPanel mPanel;
Si la directiva #include se escribe antes de los parámetros de entrada, entonces todos los parámetros de entrada escritos en el archivo incluido se encontrarán por encima de los parámetros de entrada del indicador, y esto será un inconveniente al iniciar el indicador.
Si usted hace todo correctamente e inicia el indicador, a continuación, en el inicio deberíamos ver esta imagen:
Si no necesitamos la configuración del panel, simplemente podemos borrar todas las palabras "input" en el archivo de inclusión Panel.mqh y utilizar la configuración por defecto.
Así, añadiremos el siguiente código a la función OnInit().
A continuación, comprobaremos si el panel está activado en este indicador, y cargaremos la configuración del panel guardada anteriormente, si no es la primera vez que se ejecuta el indicador en este gráfico. Si se trata de la primera ejecución, cambiaremos el tamaño del array por el número de parámetros de entrada, en este caso tres , y escribiremos los valores de estos parámetros de entrada en el array.
if(!NoPanel) { if(mPanel.Load(short_name)) { InpMAPeriod = (int)mPanel.saveBuffer[0]; InpMAShift = (int)mPanel.saveBuffer[1]; InpMAMethod = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = InpMAPeriod; mPanel.saveBuffer[1] = InpMAShift; mPanel.saveBuffer[2] = InpMAMethod; }
Nombre del panel, nombre del indicador
A continuación, todo se rellenará de la misma manera: nombre del objeto, tipo de objeto, número de línea en el panel, objeto en sí y porcentaje de la anchura del panel.
mPanel.Init("Moving Average", short_name); mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50); mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50); mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50); mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50); mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50); mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50); mPanel.Create(); }
Si ejecutamos el indicador en esta fase, obtendremos un panel como este:
Así que ya tenemos el panel, ahora lo que nos queda es escribir el código para comunicarnos con el usuario.
Ahora añadiremos una función más al indicador: OnChartEvent().
El método responsable del desplazamiento del panel se describe más arriba. Cuando se produce el evento "fin de edición de texto en el objeto gráfico Edit", comprobaremos el prefijo del objeto Edit donde ha finalizado la edición, y si el prefijo se corresponde con el prefijo de nuestro panel, comprobaremos cuál de los parámetros del indicador ha sido modificado, y registraremos los cambios en una variable y en un array para su posterior guardado.
A continuación, guardaremos todos los cambios en el archivo y reiniciaremos el indicador.
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "MAPeriod") { mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam)); } else if(sparam == mPanel.prefix + "MAShift") { mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam)); PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift); } else if(sparam == mPanel.prefix + "MAMethod") { mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam)); } mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); }
El procesamiento del botón de ocultación/representación del indicador MA es extremadamente sencillo.
Para ocultar la línea de desplazamiento, bastará con establecer el estilo de construcción gráfica DRAW_NONE, para mostrar propiamente DRAW_LINE.
if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); mPanel.HideShowInd(false); } }
Aquí terminan los cambios en el indicador Custom Moving Average. El indicador modificado tendrá el nombre Custom Moving Average Pnl.
Indicador ParabolicSAR
La modificación del indicador ParabolicSAR es absolutamente idéntica a la del indicador Custom Moving Average, salvo por matices muy pequeños.
En el indicador ParabolicSAR no es necesario crear nuevas variables con el mismo nombre que las variables de entrada, porque ya se encuentran ahí.
Por eso conectaremos directamente el archivo de inclusión:
En OnInit() añadiremos el código:
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtSarStep = mPanel.saveBuffer[0]; ExtSarMaximum = mPanel.saveBuffer[1]; } else { mPanel.Resize(2); mPanel.saveBuffer[0] = ExtSarStep; mPanel.saveBuffer[1] = ExtSarMaximum; } mPanel.Init("ParabolicSAR", short_name); mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50); mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50); mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50); mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50); mPanel.Create(); }
Luego añadiremos la función OnChartEvent() al código del indicador.
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "SARStep") mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam)); else if(sparam == mPanel.prefix + "SARMaximum") mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 159); mPanel.HideShowInd(false); } } //+------------------------------------------------------------------+
Aquí se terminarán los cambios en el indicador ParabolicSAR.
Indicador RSI
En el indicador RSI, todo se hará exactamente igual que en los dos indicadores anteriores.
En el ámbito global, justo después de la configuración de entrada, insertamos lo siguiente
#include <Panel\\Panel.mqh>
CPanel mPanel;
A continuación, en OnInit():
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtPeriodRSI = (int)mPanel.saveBuffer[0]; } else { mPanel.Resize(1); mPanel.saveBuffer[0] = ExtPeriodRSI; } mPanel.Init("RSI", short_name); mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60); mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40); mPanel.Create(); }
OnChartEvent() será ligeramente diferente de los indicadores anteriores.
El procesamiento del objeto de campo de entrada se realizará de la misma manera que en los indicadores anteriores. Pero la ocultación/representación del indicador se procesará de forma diferente, porque antes analizábamos los indicadores del gráfico principal, y el RSI es un indicador de subventana.
Al pulsar el botón "Ind Hide", pondremos a cero la altura de la ventana de indicador. Cambia el color del panel, el color y el texto del botón.
Al clicar en el botón de nuevo, ahora tendrá un nombre diferente, "Ind Show", estableceremos el valor CHART_HEIGHT_IN_PIXELS en -1. Luego cambiaremos el color del panel, el color y el texto del botón.
Cita del libro de texto:
"El establecimiento programático de la propiedad CHART_HEIGHT_IN_PIXELS hace imposible que el usuario edite el tamaño de la ventana/subventana. Para eliminar la fijación del tamaño, establezca el valor de la propiedad en -1".
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(mPanel.OnEvent(id, lparam, dparam, sparam)) { if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) if(sparam == mPanel.prefix + "PeriodRSI") { mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") // hide the subwindow indicator { if(GetButtonState(sparam)) { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0); mPanel.HideShowInd(true); } else { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1); mPanel.HideShowInd(false); } } } } //+------------------------------------------------------------------+
Otro indicador
Hay indicadores que no usan estilos gráficos en absoluto, sino que dibujan objetos gráficos, normalmente flechas. Se trata de otra variante del procesamiento del botón "Ocultar/Mostrar indicador". Veámoslo con más detalle.
No hemos buscado un indicador con flechas, simplemente hemos escrito un indicador fractal en el que los iconos superiores se muestran utilizando el dibujado PLOT_ARROW, mientras que los iconos inferiores se muestran usando el dibujado de objetos OBJ_ARROW.
A continuación le mostramos el código completo del indicador.
Los ajustes incluyen el tamaño de los hombros fractales y el número de días en los que dibujaremos OBJ_ARROW. Hemos tenido que limitar el número de días, ya que el gran número de objetos puede ralentizar mucho el calendario.
Al igual que sucede en los indicadores anteriores, justo después de las variables de entrada conectaremos el fichero Panel.mqh y declararemos una instancia de la clase CPanel.
Duplicaremos la variables input con variables normales.
#property indicator_chart_window #property indicator_plots 1 #property indicator_buffers 1 #property indicator_type1 DRAW_ARROW #property indicator_color1 clrRed #property indicator_label1 "Fractals" input int _day = 10; // day input int _barLeft = 1; // barLeft input int _barRight = 1; // barRight #include <Panel\\Panel.mqh> CPanel mPanel; double buff[]; int day = _day, barLeft = _barLeft, barRight = _barRight; datetime limitTime = 0;
En OnInit() todo será igual que en los indicadores anteriores.
//+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, buff, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); PlotIndexSetInteger(0, PLOT_ARROW, 217); PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5); string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight); IndicatorSetString(INDICATOR_SHORTNAME, short_name); if(!NoPanel) { if(mPanel.Load(short_name)) { day = (int)mPanel.saveBuffer[0]; barLeft = (int)mPanel.saveBuffer[1]; barRight = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = day; mPanel.saveBuffer[1] = barLeft; mPanel.saveBuffer[2] = barRight; } mPanel.Init("Fractals", short_name); mPanel.Record("dayText", OBJ_LABEL, 1, "Days:", 50); mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50); mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50); mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50); mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50); mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50); mPanel.Create(); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
La principal diferencia con respecto a OnChartEvent() en indicadores anteriores es que cada vez que se cambien los parámetros del indicador, los objetos dibujados por el indicador deberán eliminarse del gráfico.
Si pulsamos el botón que oculta el indicador, ocultaremos todos los objetos dibujados por el indicador en el ciclo. Y también estableceremos el tipo de construcción gráfica en DRAW_NONE.
En el proceso inverso, no solo será necesario especificar el tipo de dibujado DRAW_ARROW, sino también el número de flecha del conjunto Wingdings. Luego, en un ciclo, haremos visibles todos los objetos ocultos.
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "day") mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barLeft") mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barRight") mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") { if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetHide(name); } mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 217); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetShow(name); } mPanel.HideShowInd(false); } } } //+------------------------------------------------------------------+
También deberemos añadir al código del indicador una comprobación después de cada nuevo objeto dibujado para una bandera que indique la necesidad de ocultar el indicador y si esta bandera es verdadera, entonces ocultaremos el nuevo objeto dibujado.
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
int limit = prev_calculated - 1;
if(prev_calculated <= 0)
{
ArrayInitialize(buff, 0);
datetime itime = iTime(_Symbol, PERIOD_D1, day);
limitTime = itime <= 0 ? limitTime : itime;
if(limitTime <= 0)
return 0;
int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
limit = MathMax(rates_total - shift, barRight + barLeft);
}
for(int i = limit; i < rates_total && !IsStopped(); i++)
{
bool condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(high[j - 1] >= high[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(high[j - 1] <= high[j])
{
condition = false;
break;
}
if(condition)
buff[i - barRight] = high[i - barRight];
condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(low[j - 1] <= low[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(low[j - 1] >= low[j])
{
condition = false;
break;
}
if(condition)
{
string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
if(mPanel.hideObject)
SetHide(name);
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
}
//+------------------------------------------------------------------+
Mientras trabajaba en este artículo, di un ejemplo del uso de un panel como acceso rápido a la configuración de los objetos. Supongo que vale la pena explicar el código de dicho indicador.
Indicador Setting Objects Pnl
En este indicador no necesitaremos crear un objeto de la clase panel en OnInit(), porque el panel deberá llamarse para diferentes objetos, así que lo crearemos dinámicamente usando el operador new.
Vamos a declarar un descriptor de objeto de clase. Si se ha hecho clic en algún objeto del gráfico y se ha pulsado la tecla Shift, inicializaremos el descriptor del objeto de clase creado previamente.
Luego crearemos el panel de la misma forma que hicimos con los indicadores, con una única diferencia: transmitiremos las coordenadas actuales del ratón en el gráfico como argumentos al método Create().
Los cambios en los campos de entrada se procesarán de la misma manera que en los indicadores con una diferencia: aquí no necesitaremos guardar los cambios realizados en el archivo.
Una vez finalizada la edición, el panel podrá eliminarse haciendo clic en el botón "Del Pnl", lo cual eliminará el descriptor.
Como los distintos objetos pueden tener propiedades diferentes, habrá que dibujar el panel considerando esto. Si estamos editando una línea de tendencia, no necesitaremos para nada el campo responsable de rellenar el objeto en el panel.
Por consiguiente, no crearemos dicho campo para la línea de tendencia, sino que lo crearemos solo para los objetos que tienen algún relleno. En una situación así no podemos saber de antemano exactamente el número de líneas del panel, así que introduciremos una variable de línea, escribiremos en ella el número de la línea actual y la incrementaremos según sea necesario.
#property indicator_chart_window #property indicator_plots 0 #define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P) #include <Panel\\Panel.mqh> CPanel * mPl; //+------------------------------------------------------------------+ int OnCalculate(const int, const int, const int, const double &price[]) {return(0);} //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { static bool panel = false; if(panel) mPl.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_CLICK) if(!panel) { if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0) { int line = 1; mPl = new CPanel(); ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam); mPl.Init(EnumToString(ObjectType), sparam); mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50); mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50); line++; mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50); mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50); line++; mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50); mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50); line++; if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE) { mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50); mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50); line++; } mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100); mPl.Create(0, (int)lparam, (int)dparam); panel = true; } } else if(sparam == mPl.prefix + "delButton") { FREE(mPl); panel = false; } if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPl.prefix) >= 0) { if(sparam == mPl.prefix + "Color") SetColor(mPl.indName, StringToColor(GetText(sparam))); else if(sparam == mPl.prefix + "Style") SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Width") SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Fill") SetFill(mPl.indName, (int)StringToInteger(GetText(sparam))); ChartRedraw(); } } //+------------------------------------------------------------------+ void OnDeinit(const int reason) {FREE(mPl);} //+------------------------------------------------------------------+
El panel de propiedades de los objetos se llamará pulsando la tecla Shift + el clic izquierdo del ratón sobre el objeto.
Conclusión
Ventajas:
- En conjunto, ha resultado una solución fácil de usar.
Desventajas:
- Queríamos ocultar el indicador "hiding" en la clase CPanel, pero no lo hemos logrado.
- Si no añadimos variables de entrada al nombre corto del indicador, no podremos llamar a varios indicadores con el panel, pues sus nombres coincidirán.
- Si ejecutamos el indicador en un gráfico, cambiaremos su configuración utilizando el panel y luego guardaremos la plantilla; al cargar la plantilla no se cargarán los últimos parámetros del indicador, sino los que se han establecido en la configuración al iniciarse el indicador.
- No todos los indicadores pueden fijarse al panel.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14672





- 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