English Русский 中文 Deutsch 日本語 Português
preview
Desarrollo de un sistema de repetición (Parte 64): Presionando play en el servicio (V)

Desarrollo de un sistema de repetición (Parte 64): Presionando play en el servicio (V)

MetaTrader 5Ejemplos |
95 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, "Desarrollo de un sistema de repetición (Parte 63): Presionando play el servicio (IV)", se desarrolló e implementó un mecanismo que permite al usuario controlar, por así decirlo, la cantidad máxima de ticks que se utilizarán para el trazado de la barra en el gráfico. Aunque este control tiene como objetivo permitir que la barra se grafique sin interferir con otros puntos críticos de la aplicación de repetición/simulador, no influye ni distorsiona los valores de volumen que deben mostrarse.

Sin embargo, si has prestado atención al video del artículo o has comprobado que la aplicación de repetición/simulador funciona, habrás notado que, de vez en cuando, el sistema entraba en modo pausado. Curiosamente, esto sucedía sin que el indicador de control mostrara un cambio en su estado. Esto indicaba, de alguna manera, que habíamos pasado del modo de reproducción al modo pausa. Admito que esto es bastante extraño. Probablemente te estés preguntando cómo podía ocurrir esto. De hecho, yo también me hice esa pregunta, intentando entender por qué el sistema entraba en modo pausa en puntos específicos. Pues bien, el tema del siguiente apartado es explicar cuál fue la solución que ideé y cómo se puede entender el problema.


Entendemos y solucionamos el modo pausa automático

La gran cuestión, y el mayor inconveniente, es que el sistema de repetición/simulador entrara en modo pausa de forma automática. El verdadero problema radica en haber entendido una cuestión que, muy probablemente, será corregida pronto por los desarrolladores de MetaTrader 5, pero que, en el momento de escribir este artículo, convierte el uso de algunos tests y conceptos relacionados con ciertos objetos gráficos en una gran molestia. Esto ocurre porque dichos objetos pueden alterar su estado sin que comprendas el motivo real.

Quizá esté exigiendo demasiado. Pero recordemos lo importantes que son los objetos para nuestra aplicación de repetición/simulador y cómo los utilizamos para controlar su funcionamiento. Esto es relevante para quienes no han seguido esta serie de artículos sobre el sistema de repetición/simulador.

Al iniciar el servicio de repetición/simulador, este abre un gráfico, carga los ticks presentes en un archivo, crea un símbolo personalizado y, por último, coloca un indicador en el gráfico. Este indicador es nuestro indicador de control, cuya función es precisamente esa: controlar lo que el sistema de repetición/simulador está realizando.

En este momento, ya no utilizamos variables globales del terminal para acceder a la información o transferirla entre el indicador de control y el servicio de repetición/simulador. Ahora empleamos otra técnica mediante la cual la información fluye entre ambos programas de manera que el usuario no pueda interferir en los datos que están siendo transferidos.

Básicamente, el servicio utiliza eventos personalizados para transferir información al indicador de control. Por su parte, el indicador de control transmite parte de la información a través de un buffer hacia el servicio. Digo "parte de la información" porque existe un dato que se transmite de otra forma, con el objetivo de evitar que el servicio esté constantemente leyendo el buffer. La lectura del buffer implica transferir una cantidad de información mayor de la que realmente necesitamos. Por esta razón, hacemos que el servicio acceda directamente al objeto que mantiene el indicador de control sin modificarlo. Este objeto es el botón que representa el estado actual de ejecución, es decir, el botón mediante el cual el usuario indica si el sistema debe estar en modo de reproducción o en modo de pausa.

Puedes comprobarlo revisando el código del archivo C_Replay.mqh. Para explicar esto de manera más clara, observa el siguiente fragmento, que pertenece precisamente a la clase C_Replay.mqh.
35. //+------------------------------------------------------------------+
36. inline void UpdateIndicatorControl(void)
37.          {
38.             static bool bTest = false;
39.             double Buff[];
40.                                  
41.             if (m_IndControl.Handle == INVALID_HANDLE) return;
42.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
43.             {
44.                if (bTest)
45.                   m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1  ? C_Controls::ePause : C_Controls::ePlay);
46.                else
47.                {
48.                   if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
49.                      m_IndControl.Memory.dValue = Buff[0];
50.                   if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
51.                      if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay))
52.                         m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
53.                }
54.             }else
55.             {
56.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
57.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
58.                m_IndControl.Memory._8b[7] = 'D';
59.                m_IndControl.Memory._8b[6] = 'M';
60.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
61.                bTest = false;
62.             }
63.          }
64. //+------------------------------------------------------------------+

Fragmento original del código fuente en C_Replay.mqh

Observa que en la línea 38 tenemos una variable estática que informa al procedimiento si podemos leer el objeto directamente o si debemos leer el buffer del indicador de control. Cuando en la línea 60 enviamos un evento personalizado, forzamos en la línea 61 que la próxima llamada leerá el buffer del indicador de control, De esta forma, esta lectura no se realiza constantemente, sino solo en intervalos espaciados, evitando así afectar significativamente al servicio de repetición/simulador. Pero el gran truco, en realidad, es que en la línea 51 indicamos que las próximas lecturas no se harán a través del buffer, sino mediante acceso directo al objeto gráfico. Sin embargo, esto solo ocurrirá si estamos en modo de reproducción. Si estamos en modo pausa, el tiempo de respuesta no es un factor tan crítico.

Entonces, si estamos en modo de reproducción, a partir de la tercera llamada, se ejecutará la línea 45. Esto continuará hasta que el modo pausa sea activado explícitamente por el usuario. Cuando esto ocurra, la función LoopEventOnTime, que se encuentra en la clase C_Replay, se cerrará. Pero como el usuario solo indicó que el servicio debía entrar en modo pausa, la función LoopEventOnTime se llamará nuevamente, haciendo que el fragmento anterior vuelva a observar el indicador de control. A pesar de ello, seguiremos observando el indicador a través del objeto gráfico. Aquí se presenta un inconveniente que dificulta adoptar otro enfoque. Pero este no es el motivo del modo pausa automático. El modo pausa no se activa desde el servicio. Se activa en el indicador de control. Y la razón es precisamente el evento personalizado presente en la línea 60 del fragmento anterior. Ahora la situación se complica más. ¿cómo es posible que algo que usamos para disparar un evento personalizado esté causando que el servicio reciba erróneamente del indicador de control la información de que el usuario activó el modo pausa? Vaya, ¡qué asunto tan extraño! Es algo realmente inusual. Pero, de alguna manera, el evento personalizado está haciendo que el indicador cambie el valor de la propiedad OBJPROP_STATE del objeto que el servicio está observando. Cuando esta propiedad cambia, la línea 45 del fragmento anterior provoca que el servicio reciba una notificación falsa de que el indicador ha entrado en modo pausa. Esto hace que la función LoopEventOnTime salga y se reinicie. Sin embargo, cuando esta función vuelve a verificar el valor de la propiedad OBJPROP_STATE, encuentra un valor incorrecto. Esto hace que el servicio entre en modo pausa, mientras que el indicador sigue mostrando al usuario que el sistema está operando normalmente, es decir, en modo play.

Muy bien. Si realmente has comprendido el error, seguramente pensarás que el problema radica en que estamos observando un objeto en el gráfico en lugar de verificar el contenido del buffer del indicador. Y tienes razón. Todo el problema se debe a que hacemos que el servicio observe el valor de la propiedad OBJPROP_STATE de un objeto en el gráfico que, en realidad, el servicio no debería consultar. Estoy completamente de acuerdo. Pero esto no justifica que la propiedad OBJPROP_STATE se modifique debido a que se ha disparado un evento personalizado. Un error no justifica el otro. En cualquier caso, existen dos soluciones más directas para este problema. Una sería utilizar otra propiedad que permita al servicio observar únicamente lo que un objeto del gráfico está haciendo. Aunque esta solución resolvería el problema, no la voy a implementar. La razón es que existe otro problema que debemos corregir, o más bien algo que aún necesitamos implementar.

Aunque esto nos suponga un pequeño incremento en el tiempo de acceso al buffer del indicador en lugar de observar un objeto en el gráfico, optaré por consultar el buffer del indicador. Esto se debe a que implementaré una función que actualmente no está presente, pero que sí estuvo disponible en versiones anteriores de este servicio de repetición/simulador. Este recurso no es otro que el avance rápido. Por lo tanto, después de realizar las modificaciones y eliminar la variable estática de la rutina, el resultado quedó como se muestra en el fragmento siguiente.

35. //+------------------------------------------------------------------+
36. inline void UpdateIndicatorControl(void)
37.          {
38.             double Buff[];
39.                                  
40.             if (m_IndControl.Handle == INVALID_HANDLE) return;
41.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
42.             {
43.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
44.                   m_IndControl.Memory.dValue = Buff[0];
45.                if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState)
46.                   if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
47.                      m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
48.             }else
49.             {
50.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
51.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
52.                m_IndControl.Memory._8b[7] = 'D';
53.                m_IndControl.Memory._8b[6] = 'M';
54.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
55.             }
56.          }
57. //+------------------------------------------------------------------+

Fragmento modificado del código fuente en C_Replay.mqh

A pesar de todas las molestias, esta será la mejor solución para nosotros. Aunque esto signifique que existe algo extraño y que las propiedades de determinados objetos no son completamente fiables. Al menos, eso es lo que puedo afirmar hasta el momento en que escribo este artículo. Sin embargo, si necesitas que un servicio lea datos de algún objeto presente en el gráfico por cualquier motivo, te recomiendo usar la propiedad OBJPROP_TOOLTIP. Aunque es del tipo string, no encontré problemas al usarla en las pruebas de transferencia de información para determinar si el indicador de control estaba en modo pausa o modo play. Sin embargo, aunque esta solución para acceder al objeto era adecuada, no nos permitiría implementar el avance rápido. Serían necesarias varias modificaciones adicionales en el código y, al final, igualmente tendríamos que modificar el procedimiento UpdateIndicadorControl, aunque este seguiría tal y como se muestra.

Muy bien, ahora que ya hemos solucionado este problema, abordemos otro igual de molesto que se presentó en video en el artículo "Desarrollando un sistema de repetición (Parte 62). Presionando play en el servicio (III)". Para explicar cómo se resolvió, pasaremos a otro tema.


Resolvemos el problema de fuga de memoria

Es muy probable que muchos de ustedes se hayan sorprendido al ver cómo la aplicación fallaba de repente sin previo aviso. Esto ocurría al cerrar la aplicación, debido a que se estaba cerrando el gráfico o ya se había cerrado. Un programador menos experimentado podría atribuir el problema de inmediato a algo relacionado con MetaTrader 5 o con MQL5. De hecho, es más fácil culpar a otro que asumir la responsabilidad de que tu programa está presentando un comportamiento inadecuado o contiene errores que aún no te has tomado el tiempo de corregir o examinar. Esto puede deberse a la falta de conocimiento o experiencia suficiente para abordar los errores o a que estás más enfocado en desarrollar una característica específica antes que en corregir fallos. Básicamente, esperar que la plataforma lo haga todo por ti es firmar un certificado de ignorancia o ingenuidad. En realidad, la plataforma solo hará aquello para lo que ha sido diseñada. Depende de ti, como programador, realizar el resto del trabajo: corregir errores y asegurarte de que tu aplicación funcione correctamente.

Durante un tiempo estuve descuidando y posponiendo la resolución de ciertos problemas y la mejora de partes del código. Estaba tan concentrado en desarrollar otras cosas que lo dejé todo de lado. No estaba seguro de si sería posible alcanzar ciertos objetivos. En tales casos, uno simplemente no se preocupa por corregir errores o mejorar el código; lo único que desea es que funcione o tenga ciertas características. Invertir tiempo en resolver fallos para luego desechar todo el código corregido porque algo resultó inviable de implementar o mantener puede resultar desalentador. Por eso, las correcciones y mejoras solo ocurren cuando el código demuestra ser lo suficientemente sólido como para recibir la atención necesaria.

Quizá muchos piensen que estoy dando demasiadas vueltas alrededor de algo. Podría mostrar un código mucho más avanzado desde el principio. Pero la verdad es que eso crearía una falsa ilusión entre quienes están comenzando, haciéndoles creer que el código nace listo, o peor aún, que debería nacer con todas las funcionalidades y características ya implementadas. Sin embargo, quienes llevan años programando saben que las cosas no funcionan así. Quiero mostrar cómo se desarrolla realmente un código y cómo, como programadores, enfrentamos y solucionamos los problemas que van surgiendo.

De todos modos, ha llegado el momento de mostrar cómo corregir el fallo que ha estado presente en el código desde que comenzamos a hacer que funcionara de una manera diferente. Si no has revisado el código para tratar de entender por qué ocurre, podrías pensar que se debe a diversos defectos. Pero no es así. Siento decirte que, si has pensado así, probablemente no estás buscando estudiar y comprender los artículos. Lo que buscas es simplemente un código listo, sin interés en aprender a programar. Y justamente el propósito de estos artículos es enseñarte, estimado lector, algo que otro programador ha probado o está desarrollando, para que puedas aprender nuevas técnicas o descubrir diferentes formas de obtener ciertos resultados.

Pero basta de rodeos. Veamos cómo resolver el problema de la fuga de memoria. Lo primero que debes hacer es observar el contenido de los mensajes que informa MetaTrader 5. Para facilitarte la tarea, puedes ver uno de estos mensajes en la imagen justo a continuación.

Image 01

Imagen 01 - Visualización de los fallos indicados por MetaTrader 5

Observa que en esta imagen hay dos líneas destacadas. El resto de líneas están relacionadas con estas dos. Si te fijas bien, podrás ver a qué se debe el fallo. Esto ocurre porque algunos objetos no se están eliminando del gráfico. Entonces podrías preguntarte: ¿cómo es posible? ¿Cómo es posible que los objetos no se estén eliminando del gráfico? ¿Acaso olvidé eliminarlos? No. Los objetos sí están siendo eliminados y dicha eliminación ocurre en el destructor de la clase. Esto lo puedes confirmar revisando el código del indicador de control. El fallo ocurre precisamente allí, como se muestra en la imagen anterior.

Entonces, si los objetos están siendo eliminados, ¿por qué MetaTrader 5 nos alerta sobre objetos que no se están eliminando? Y lo que es peor, estos objetos pertenecen a la clase C_DrawImage.

Ante esta situación, es normal que te sientas perdido y no sepas qué hacer. Esto se debe a que la clase C_DrawImage no se accede directamente, sino de manera indirecta. Para explicarlo mejor, revisemos el código de la clase C_Controls, que se encarga de gestionar todos los objetos. Uno de sus trabajos es, precisamente, acceder a dicha clase. Recuerda: MetaTrader 5 nos está alertando sobre un fallo relacionado con la eliminación de objetos del tipo C_DrawImage. Por lo tanto, el problema no está en la clase C_DrawImage, sino en el código que utiliza dicha clase, es decir, la clase C_Controls.

Dado que no es necesario ver el código completo para entender el problema y la solución, a continuación tienes acceso a los fragmentos más importantes. Sin embargo, hay un pequeño detalle: en el código que ves a continuación ya está implementada la solución al problema de fuga de memoria.

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "..\Auxiliar\C_DrawImage.mqh"
005. #include "..\Defines.mqh"
006. //+------------------------------------------------------------------+
007. #define def_PathBMP            "Images\\Market Replay\\Control\\"
008. #define def_ButtonPlay         def_PathBMP + "Play.bmp"
009. #define def_ButtonPause        def_PathBMP + "Pause.bmp"
010. #define def_ButtonLeft         def_PathBMP + "Left.bmp"
011. #define def_ButtonLeftBlock    def_PathBMP + "Left_Block.bmp"
012. #define def_ButtonRight        def_PathBMP + "Right.bmp"
013. #define def_ButtonRightBlock   def_PathBMP + "Right_Block.bmp"
014. #define def_ButtonPin          def_PathBMP + "Pin.bmp"
015. #resource "\\" + def_ButtonPlay
016. #resource "\\" + def_ButtonPause
017. #resource "\\" + def_ButtonLeft
018. #resource "\\" + def_ButtonLeftBlock
019. #resource "\\" + def_ButtonRight
020. #resource "\\" + def_ButtonRightBlock
021. #resource "\\" + def_ButtonPin
022. //+------------------------------------------------------------------+
023. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A))
024. #define def_PosXObjects         120
025. //+------------------------------------------------------------------+
026. #define def_SizeButtons         32
027. #define def_ColorFilter         0xFF00FF
028. //+------------------------------------------------------------------+
029. #include "..\Auxiliar\C_Mouse.mqh"
030. //+------------------------------------------------------------------+
031. class C_Controls : private C_Terminal
032. {
033.    protected:
034.    private   :
035. //+------------------------------------------------------------------+
036.       enum eMatrixControl {eCtrlPosition, eCtrlStatus};
037.       enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)};
038. //+------------------------------------------------------------------+
039.       struct st_00
040.       {
041.          string   szBarSlider,
042.                   szBarSliderBlock;
043.          ushort   Minimal;
044.       }m_Slider;
045.       struct st_01
046.       {
047.          C_DrawImage *Btn;
048.          bool         state;
049.          short        x, y, w, h;
050.       }m_Section[eObjectControl::eNull];
051.       C_Mouse   *m_MousePtr;
052. //+------------------------------------------------------------------+

              ...

071. //+------------------------------------------------------------------+
072.       void SetPlay(bool state)
073.          {
074.             if (m_Section[ePlay].Btn == NULL)
075.                m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay);
076.             m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0);
077.             if (!state) CreateCtrlSlider();
078.          }
079. //+------------------------------------------------------------------+
080.       void CreateCtrlSlider(void)
081.          {
082.             if (m_Section[ePin].Btn != NULL) return;
083.             CreteBarSlider(77, 436);
084.             m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock);
085.             m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock);
086.             m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin);
087.             PositionPinSlider(m_Slider.Minimal);
088.          }
089. //+------------------------------------------------------------------+
090. inline void RemoveCtrlSlider(void)
091.          {         
092.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
093.             for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++)
094.             {
095.                delete m_Section[c0].Btn;
096.                m_Section[c0].Btn = NULL;
097.             }
098.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B"));
099.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
100.          }
101. //+------------------------------------------------------------------+

              ...

132. //+------------------------------------------------------------------+
133.    public   :
134. //+------------------------------------------------------------------+
135.       C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr)
136.          :C_Terminal(Arg0),
137.           m_MousePtr(MousePtr)
138.          {
139.             if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown);
140.             if (_LastError != ERR_SUCCESS) return;
141.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false);
142.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
143.             ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true);
144.             for (eObjectControl c0 = ePlay; c0 < eNull; c0++)
145.             {
146.                m_Section[c0].h = m_Section[c0].w = def_SizeButtons;
147.                m_Section[c0].y = 25;
148.                m_Section[c0].Btn = NULL;
149.             }
150.             m_Section[ePlay].x = def_PosXObjects;
151.             m_Section[eLeft].x = m_Section[ePlay].x + 47;
152.             m_Section[eRight].x = m_Section[ePlay].x + 511;
153.             m_Slider.Minimal = eTriState;
154.          }
155. //+------------------------------------------------------------------+
156.       ~C_Controls()
157.          {
158.             for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn;
159.             ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName(""));
160.             delete m_MousePtr;
161.          }
162. //+------------------------------------------------------------------+

              ...

172. //+------------------------------------------------------------------+
173.       void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
174.          {
175.             short  x, y;
176.             static ushort iPinPosX = 0;
177.             static short six = -1, sps;
178.             uCast_Double info;
179.             
180.             switch (id)
181.             {
182.                case (CHARTEVENT_CUSTOM + evCtrlReplayInit):
183.                   info.dValue = dparam;
184.                   if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break;
185.                   iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition]));
186.                   SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay);
187.                   break;
188.                case CHARTEVENT_OBJECT_DELETE:
189.                   if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName(""))
190.                   {
191.                      if (sparam == def_ObjectCtrlName(ePlay))
192.                      {
193.                         delete m_Section[ePlay].Btn;
194.                         m_Section[ePlay].Btn = NULL;
195.                         SetPlay(m_Section[ePlay].state);
196.                      }else
197.                      {
198.                         RemoveCtrlSlider();
199.                         CreateCtrlSlider();
200.                      }
201.                   }
202.                   break;
203.                case CHARTEVENT_MOUSE_MOVE:
204.                   if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft))   switch (CheckPositionMouseClick(x, y))
205.                   {
206.                      case ePlay:
207.                         SetPlay(!m_Section[ePlay].state);
208.                         if (m_Section[ePlay].state)
209.                         {
210.                            RemoveCtrlSlider();
211.                            m_Slider.Minimal = iPinPosX;
212.                         }else CreateCtrlSlider();
213.                         break;

              ...

249. //+------------------------------------------------------------------+

Fragmentos del código fuente de C_Controls.mqh

Como mencioné anteriormente, si estás empezando en el campo de la programación, es posible que no logres entender inmediatamente cómo se obtuvo la solución. Esto se debe a que, al observar rápidamente el código, no notarás ninguna diferencia significativa. Esto ocurre porque la solución no es un código extenso ni supone un gran cambio en la clase. La corrección consistió en agregar una única línea al código de la clase. Así es, lo que acabas de leer es cierto. Para resolver el problema de la fuga, fue necesario añadir un código extremadamente simple, pero crucial, que es difícil de imaginar para alguien que recién comienza. Sin embargo, está compuesto por una sola línea que cualquiera podrá entender al mirarla. Se trata de algo banal, pero si no está presente, producirá la mencionada fuga de memoria, lo que indica que los objetos pertenecientes a la clase C_DrawImage no están siendo eliminados correctamente.

Ahora, veamos por qué el problema se origina en la clase C_Controls y no en la C_DrawImage. Como mencioné antes, la clase C_DrawImage no se accede directamente, sino de manera indirecta. Esto ocurre porque el acceso a la clase C_DrawImage se realiza a través de un puntero, que está declarado en la línea 47. Presta atención: la clase C_DrawImage está referenciada por un puntero. Sin embargo, dado que en MQL5 los punteros son ligeramente diferentes a los de C/C++, puedes considerar la línea 47 como una variable. Sin embargo, sigue siendo un puntero. El problema no está en declarar la variable como puntero; eso no es el problema. El problema radica en cómo estamos trabajando con este puntero.

El gran inconveniente de los punteros, y quizá la razón por la que son diferentes en MQL5 que en C/C++, es que una incidencia relacionada con ellos puede ser muy difícil de resolver. Algunos de estos errores son tan complejos que solo ocurren bajo ciertas interacciones, por lo que su corrección es muy complicada. Puedes probar el programa cientos de veces, pero en una de esas interacciones el problema se presenta, y muchas veces sucede justamente cuando no estás depurando el programa. Esta es, sin duda, la peor situación posible.

Ahora bien, como buena práctica de programación, cuando declaramos algo como un puntero o que funcione como tal, debemos inicializarlo lo antes posible. Esto también se aplica a las variables en general. Dado que nuestro código es una clase, la inicialización debe hacerse en el constructor de dicha clase. Esto ya estaba ocurriendo y puedes verlo en las líneas 144 a 149.Para ser exactos, el puntero se inicializa en la línea 148. Nota que lo inicializamos con el valor NULL.

Ahora viene la cuestión. El constructor de la clase puede invocarse de dos maneras diferentes. Una es cuando la clase se referencia como una variable normal. En este caso, no hay mayores problemas, ya que es el propio compilador el que realiza la reserva de memoria necesaria para que la clase funcione de manera estable y fluida. La segunda manera es cuando la clase se referencia mediante punteros. En este caso, el programador debe invocar el constructor de la clase mediante el operador new y llamar al destructor mediante el operador delete.

Muy bien. En la línea 156 se implementa el destructor de la clase C_Controls. Ahora presta mucha, pero muchísima atención al siguiente hecho, ya que es crucial para entender el fallo. Cuando el destructor de la línea 156 ejecuta la línea 158, llama al destructor de la clase C_DrawImage. Este destructor podría existir o no, pero lo importante es que, al ejecutarse, también libera la memoria que fue asignada por el puntero. Si esto se lleva a cabo correctamente, MetaTrader 5 no genera ningún tipo de alerta por problemas de fuga de memoria. Es decir, la memoria se libera sin fallos. Sin embargo, no es lo que está ocurriendo. Entonces, la pregunta es: ¿por qué?

La razón es que, en algún punto del código, algo está interfiriendo con el operador DELETE y el único elemento capaz de hacerlo es el operador NEW. Por lo tanto, el problema radica precisamente en el operador NEW. Pero, ¿por qué falla? La razón es que el operador NEW en MQL5 parece funcionar de la misma manera que en C/C++. En algunos lenguajes de programación, el operador NEW tiene un comportamiento diferente, pero no es el caso aquí. Entendamos entonces qué está sucediendo realmente.

Cuando los objetos que deben ser manejados por la clase C_DrawImage necesitan crearse, esto se hace utilizando el operador new. Este tipo de operación se puede observar en las siguientes líneas:

  • En la línea 75, se crea el botón que representa la imagen de play y pause.
  • En las líneas 84 a 86 se crean los botones que se usarán en el control deslizante.

Sin embargo, solo en dos puntos se libera efectivamente la memoria de la clase C_DrawImage. Estos puntos son: en el destructor de la clase C_Controls y en la línea 95. Ahora quiero destacar el siguiente hecho: en la línea 96, inmediatamente después de liberar la memoria utilizando el operador DELETE, se asigna nuevamente el valor NULL al puntero. ¿Por qué se hacía esto? La razón es que, si intentas referenciar una región de memoria sin que el puntero apunte a algo válido, podrías leer datos basura, escribir sobre información crítica o incluso ejecutar código malicioso. Por esta razón, es una buena práctica de programación asegurarte siempre de que los punteros inválidos tengan un valor NULL. SIEMPRE. Sin embargo, este código ya existía, y aun así el fallo ocurría.

Volvamos entonces a los puntos de acceso de los operadores NEW. La línea 74 ya existía desde el principio de la codificación de esta clase. Esto garantiza que, cuando el puntero no esté en uso, apuntará a algo. Por tanto, el fallo no está aquí. Sin embargo, la línea 82 no existía originalmente. ¿Por qué no existía? No puedo responder con certeza a esta pregunta. Tal vez fue un olvido. Tal vez porque no imaginé que realmente haría alguna diferencia. La verdad es que no lo sé. Pero precisamente la ausencia de esta línea 82 es lo que provocaba que MetaTrader 5 alertara de un fallo.

No sé si te das cuenta de la importancia que puede tener algo tan banal como un simple test para verificar si el puntero estaba o no ocupado. Tal vez el problema ni siquiera fue un error por mi parte, ya que el procedimiento CreateCtrlSlider solo se debería llamar si se había ejecutado previamente RemoveCtrlSlider. Sin embargo, hay un punto, uno solo, donde esto no sucede. Y este punto es la línea 212. Este problema provoca que el operador NEW ejecute nuevamente llamadas al constructor de la clase C_DrawImage, lo que asigna más memoria y duplica los objetos. La memoria asignada previamente no se libera, por lo que se acumula y ocupa cada vez más espacio en la memoria. Hasta que se cierra la aplicación. En ese momento, MetaTrader 5 indica que ha habido un fallo en la aplicación.


Conclusión

Como mencioné antes, algunos lenguajes tienen un mecanismo que impide que un puntero ya en uso apunte a otra cosa. Sin embargo, hasta el momento no parece ser el caso en MQL5, según he podido observar. Lo que estoy mostrando aquí no es un fallo de MQL5, sino algo de lo que debes estar atento y con lo que debes saber manejarte al programar. Aunque muchos dicen y aseguran que MQL5 no tiene punteros, puedes ver que esto no es del todo cierto. Y sí, si no tomas las precauciones adecuadas, podrías enfrentarte a problemas graves, incluso en programas simples y aparentemente sin mayores complicaciones de implementación.

Durante años he lidiado con fallos como este, en los que un puntero simplemente se vuelve rebelde. Y, aun con toda la experiencia acumulada a lo largo del tiempo, sigo tropezando con errores relacionados con punteros. Todo se debe a no haber realizado la prueba que aparece en la línea 82, que garantiza que no se utilizará un puntero ya ocupado. Además, no presté suficiente atención a la llamada de la línea 212, que se ejecuta con cada movimiento del mouse, cuando las condiciones lo permiten, y está dentro de un evento del mouse.

Espero que este conocimiento te sea útil y que te sirva de advertencia. Nunca subestimes a los punteros. Son herramientas muy útiles, pero también pueden causar grandes dolores de cabeza. En el siguiente video puedes ver cómo quedó el sistema sin el fallo.

Video de demostración

Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/12250

Archivos adjuntos |
Anexo.zip (420.65 KB)
Desarrollo de un sistema de repetición (Parte 65): Presionando play en el servicio (VI) Desarrollo de un sistema de repetición (Parte 65): Presionando play en el servicio (VI)
En este artículo, mostraré cómo lo implementaremos y resolveremos el problema del indicador del mouse cuando se utiliza junto con la aplicación de repetición/simulación. El contenido expuesto aquí tiene como único propósito la enseñanza. En ningún caso debe considerarse una aplicación cuya finalidad no sea el aprendizaje y estudio de los conceptos presentados.
Desarrollo de un sistema de repetición (Parte 63): Presionando play en el servicio (IV) Desarrollo de un sistema de repetición (Parte 63): Presionando play en el servicio (IV)
En este archivo, resolveremos por fin los problemas de simulación de los ticks en una barra de un minuto, de manera que puedan coexistir con ticks reales. De esta manera, evitaremos enfrentarnos a problemas en el futuro. El contenido expuesto aquí tiene como único objetivo la didáctica. En ningún caso debe interpretarse como una aplicación cuya finalidad no sea el aprendizaje y el estudio de los conceptos mostrados.
Elementos del análisis de correlación en MQL5: Prueba chi-cuadrado de Pearson de independencia y ratio de correlación. Elementos del análisis de correlación en MQL5: Prueba chi-cuadrado de Pearson de independencia y ratio de correlación.
El artículo analiza las herramientas clásicas del análisis de correlaciones. Se hace hincapié en los breves antecedentes teóricos, así como en la aplicación práctica de la prueba de independencia chi-cuadrado de Pearson y la ratio de correlación.
Modificaciones más notables del algoritmo de búsqueda cooperativa artificial (Artificial Cooperative Search, ACSm) Modificaciones más notables del algoritmo de búsqueda cooperativa artificial (Artificial Cooperative Search, ACSm)
Aquí consideraremos la evolución del algoritmo ACS: tres modificaciones destinadas a mejorar las características de convergencia y la eficiencia del algoritmo. Transformación de uno de los principales algoritmos de optimización. De las modificaciones matriciales a los planteamientos revolucionarios en materia de formación de la población.