OOP en MQL5 con un Ejemplo: Procesamiento de Códigos de Advertencia y Error

KlimMalgin | 21 febrero, 2014

Breve Introducción a OOP

Antes de empezar con el proceso de desarrollo, familiaricémonos con algunas herramientas del OOP que se usarán en este artículo. 

Por supuesto, usaremos estructuras y clases. Estos son los elementos básicos de los lenguajes orientados al objeto. ¿Qué es una estructura, qué es una clase, y en qué se diferencian?

Una estructura es una construcción que permite contener un conjunto de variables y funciones de diferentes tipos (excepto void).

Una clase, al igual que una estructura, es un conjunto de campos de datos. Pero una clase es una construcción más complicada y "flexible". Las clases son el concepto básico del OOP. Las diferencias de clases y estructuras se describen en la documentación. Repito:

  • La clase de palabras clave se usa en declaraciones.
  • Por defecto, el especificador de acceso de todos los miembros de una clase es privado, a no ser que se indique lo contrario. Los miembros de datos de una estructura son de acceso público por defecto, a no ser que se indique lo contrario.
  • Los objetos de una clase siempre tiene una tabla de funciones virtuales, aún cuando las funciones virtuales no están declaradas en la clase. Las estructuras no pueden tener funciones virtuales.
  • El operador new (nuevo) se puede aplicar a objetos de clase. Este operador no se puede aplicar a estructuras.
  • Las clases pueden heredarse solo de clases. Las estructuras pueden heredarse solo de estructuras.

 

Ahora, consideremos las clases. Todos los campos de clase se dividen en dos tipos. Estos son miembros de datos (variables, arrays, etc.) y funciones definidos dentro de una clase.

Los miembros de datos se llaman generalmente propiedades, porque cuando se crea un objeto de una clase, los miembros de datos reflejan exactamente las propiedades de este objeto. Por ejemplo, si se trata de una forma geométrica, un círculo por ejemplo, las propiedades contendrán su radio, anchura de borde, color de forma... En resumen, las propiedades del objeto.

Las funciones definidas dentro de una clase se llaman métodos. Se usan tanto para trabajar con propiedades de clase como para implementar cualquier otro algoritmo programado en ellos.

La programación orientada al objeto tiene la noción de encapsulación. Esto es la posibilidad de esconder datos e implementación de un objeto de la influencia directa de un usuario (programador de la aplicación). Un programador solo obtiene documentación que describe los métodos que se pueden usar para cambiar las propiedades de un objeto. El cambio directo de propiedades de objeto es imposible.

Estas medidas de protección son necesarias en casos en los que se requiere un número de revisiones antes de cambiar el valor de una propiedad. Todas las revisiones requeridas se implementan en métodos, y en caso de que se lleven a cabo con éxito, permitirán el cambio del valor de la propiedad. Y en el caso de que un usuario tenga acceso directo a las propiedades, estas revisiones no se llevarán a cabo. Por tanto, el valor de la propiedad resultante se podría configurar incorrectamente, y el programa MQL no funcionará como debería..

Podemos establecer un nivel de acceso para todas las propiedades y métodos de una clase usando modificadores: private (privados), protected (protegidos) y public (públicos).

Si el modificador private se usa para un campo de clase, el acceso a este campo será imposible usando solo métodos de la misma clase, y por tanto no se puede modificar desde fuera. El modificador protected también impone algunas restricciones en el acceso al campo desde fuera, pero hace posible los campos de clase para los métodos de la misma clase y para los métodos de subclases. Por el contrario, el modificador public elimina todas las restricciones de acceso y abre un acceso libre a los campos de clase.

Crear un archivo mqh "Include"

La clase que vamos a escribir debe localizarse en un archivo mqh separado para que se pueda incluir en nuestros programas (Asesores Expertos, scripts, indicadores)

Para crear este archivo, usemos el MQL5 Wizard. En el menú File (Archivo) -> Create (Crear) seleccione archivo Include (incluir) (*.mqh) y vaya a Next (Siguiente). En la venta que aparece, introduzca el nombre del archivo (yo lo he llamado ControlErrors) y pulse en Done (Hecho). Se abrirá una plantilla de archivo mqh. Continuaremos trabajando en este archivo.

Para Empezar

Ahora ya sabe todos los fundamentos teóricos del OOP que pueden ser útiles en el proceso de estudio de este artículo. De modo que sigamos adelante.

Consideremos el código de clase con la declaración de todas sus propiedades y métodos:

class ControlErrors
{
private:

   // Flags determining what types of statements should be introduced
   bool _PlaySound;    // Play or don't play a sound when an error occurs
   bool _PrintInfo;    // Enter error data the the journal of Expert Advisors
   bool _AlertInfo;    // Generate Alert alert with error data
   bool _WriteFile;    // Write reports on errors onto a file or not
   
   // A structure for storing error data and elements that use this structure
   struct Code
   {
      int code;      // Error code
      string desc;   // Description of an error code
   };
   Code Errors[];    // Array that contains error codes and their descriptions
   Code _UserError;  // Stores information about a custom error
   Code _Error;      // Stores information about the last error of any type   
   
   // Different service properties
   short  _CountErrors;     // Number of errors stored in array Errors[]
   string _PlaySoundFile;   // File that will be played for an alert sound
   string _DataPath;        // Path to the log storing directory

   
public:
   // Constructor
   ControlErrors(void);
   
   // Methods for setting flags
   void SetSound(bool value);          // Play or don't play a sound when an error occurs
   void SetPrint(bool value);          // Enter error data the the journal of Expert Advisors or not
   void SetAlert(bool value);          // Generate an Alert message or not
   void SetWriteFlag(bool flag);       // Set the writing flag. true - keep logs, false - do not keep
   
   // Methods for working with errors
   int  mGetLastError();            // Returns contents of the system variable _LastError
   int  mGetError();                // Returns code of the last obtained error
   int  mGetTypeError();            // Returns error type (Custom = 1 ore predefined = 0)
   void mResetLastError();          // Resets the contents of the system variable _LastError
   void mSetUserError(ushort value, string desc = "");   // Sets the custom error
   void mResetUserError();          // Resets class fields that contain information about the custom error
   void mResetError();              // Resets the structure that contains information about the last error
   string mGetDesc(int nErr = 0);   // Returns error description by the number, or that of the current error of no number
   int Check(string st = "");       // Method to check the current system state for errors
   
   // Alert methods(Alert, Print, Sound)
   void mAlert(string message = "");
   void mPrint(string message = "");
   void mSound();
      
   // Various service methods
   void SetPlaySoundFile(string file); // Method sets the file name to play an sound
   void SetWritePath(string path);     // Set the path to store logs  
   int mFileWrite();                   // Record into a file the available information about the last error
};

Propiedades de Clase

En primer lugar viene la declaración de propiedades de clase. El modificador private se aplica a todas las propiedades, de modo que es imposible trabajar con propiedades directamente fuera de la clase. Las propiedades se dividen en tres clases por motivos de conveniencia:

  1. Flags que determinan qué tipos de informes se deben mantener. Todas estas flags pueden aceptar solo dos tipos de valores: "true", que significa que este tipo de informe (notificaciones) está activado, y "false", que significa que está desactivado.
    • _PlaySound - variable que activa y desactiva la reproducción de una melodía o sonido seleccionado cuando se da un error.
    • _PrintInfo - responsable de añadir detalles de error en el diario del Asesor Experto.
    • _AlertInfo - activa o desactiva la salida de Alert con información de error.
    • _WriteFile - activa o desactiva la grabación de la información del error en un archivo.
  2. Estructura de almacenamiento de datos y elementos que usan esta estructura.
    • Código - la estructura concreta. Se creó por la conveniencia de almacenar datos de error en un array.
    • Errores - un array de tipo Code, es decir, cada elemento del array es una estructura de código.
    • _UserError - una variable de tipo Code. Se usa para trabajar con errores personalizados.
    • _Error - una variable de tipo Code. El último error ocurrido se coloca en esta variable, y el resto del trabajo con este error se implementa a través de esta variable.
  3. Otras propiedades del servicio:
    • _CountErrors - la variable contiene el número de errores, cuyos datos deben almacenarse en el array "Errors". Se usa para especificar el tamaño del array.
    • _PlaySoundFile - contiene el nombre del archivo que se reproducirá como sonido de alerta.
    • _DataPath - contiene la ruta y nombre del archivo de registro donde se escriben los datos de error.

Creo que con esto, todo lo referente al primer grupo de propiedades queda claro: activan o desactivan ciertos informes. En el segundo grupo, la estructura del código es interesante. ¿Qué es, y por qué se usa esta estructura en los elementos del array? ¡Es muy fácil! Es mucho más conveniente almacenar todos los datos requeridos en un solo elemento del array que configurar arrays diferentes para un código de error y su descripción. Se usa una estructura para implementar tal posibilidad. Todos los campos requeridos se declaran dentro de una estructura. En nuestro caso, son:

En realidad la estructura es un tipo de datos compuesto, es decir, se puede usar para declarar variables y arrays, que ya se hizo. Como consecuencia, cada variable del tipo Code contendrá campos de esta estructura. Igualmente, cada elemento del array del tipo Code contendrá dos campos para almacenar el código y su descripción. Por tanto, así se implementa una forma muy conveniente para almacenar datos de objeto de diferentes tipos en un mismo sitio en MQL5.

A continuación vienen las variables _UserError y _Error. Ambas contienen información sobre el último error ocurrido, pero _UserError almacena información sobre errores personalizados, mientras que _Error contiene todos los errores.

Y ahora seguiremos con el tercer grupo de propiedades. Aquí he incluido todas las propiedades restantes que no se pueden incluir en los dos primeros grupos. Son tres. La primera es _CountErrors, que contiene el número de errores cuya información se almacena en array _Errors. Esta propiedad se usa para configurar el tamaño del array _Errors en el constructor y en algunos métodos para llamar elementos de array. La segunda propiedad es _PlaySoundFile. Almacena el nombre de un archivo de sonido que se reproduce cuando se da un error. La tercera propiedad es _DataPath. Almacena la ruta y nombre del archivo para mantener registros.

Métodos de Clase. Constructor

A continuación describiremos el constructor y los métodos de clase. Empecemos con el constructor y tratemos de entender qué es. Al igual que los métodos, es una función común definida dentro de una clase, pero que posee algunas cualidades específicas:

Generalmente, los miembros de una clase se inicializan en constructores. Por ejemplo, en nuestro constructor de clase hemos configurado todas las flags para desactivar el mantenimiento de informes, hemos especificado el nombre de un archivo de sonido y un archivo de registro, hemos establecido el tamaño del array _Errors, y hemos llenado este array con información. A continuación publicaré solo parte del código del constructor, porque es demasiado grande y del mismo tipo. La parte principal se ocupa llenando el array _Errors con códigos y sus descripciones. El código completo se encuentra adjunto a este artículo.

void ControlErrors::ControlErrors(void)
{
   SetAlert(false);
   SetPrint(false);
   SetSound(false);
   SetWriteFlag(false);
   SetPlaySoundFile("alert.wav");
   SetWritePath("LogErrors.txt");
   
   _CountErrors = 150;
   ArrayResize(Errors, _CountErrors);

   // Return codes of a trade server
   Errors[0].code = 10004;Errors[0].desc = "Requote";
   Errors[1].code = 10006;Errors[1].desc = "Request rejected";
   Errors[2].code = 10007;Errors[2].desc = "Request canceled by trader";
   Errors[3].code = 10008;Errors[3].desc = "Order placed";
   Errors[4].code = 10009;Errors[4].desc = "Request is completed";
   Errors[5].code = 10010;Errors[5].desc = "Request is partially completed";
   Errors[6].code = 10011;Errors[6].desc = "Request processing error";
   Errors[7].code = 10012;Errors[7].desc = "Request canceled by timeout";
   Errors[8].code = 10013;Errors[8].desc = "Invalid request";
   Errors[9].code = 10014;Errors[9].desc = "Invalid volume in the request";
   Errors[10].code = 10015;Errors[10].desc = "Invalid price in the request";
   Errors[11].code = 10016;Errors[11].desc = "Invalid stops in the request";
   Errors[12].code = 10017;Errors[12].desc = "Trade is disabled";
   Errors[13].code = 10018;Errors[13].desc = "Market is closed";
   Errors[14].code = 10019;Errors[14].desc = "There is not enough money to fulfill the request";
   Errors[15].code = 10020;Errors[15].desc = "Prices changed";
   Errors[16].code = 10021;Errors[16].desc = "There are no quotes to process the request";
   Errors[17].code = 10022;Errors[17].desc = "Invalid order expiration date in the request";
   Errors[18].code = 10023;Errors[18].desc = "Order state changed";
   Errors[19].code = 10024;Errors[19].desc = "Too frequent requests";
   Errors[20].code = 10025;Errors[20].desc = "No changes in request";
   Errors[21].code = 10026;Errors[21].desc = "Autotrading disabled by server";
   Errors[22].code = 10027;Errors[22].desc = "Autotrading disabled by client terminal";
   Errors[23].code = 10028;Errors[23].desc = "Request locked for processing";
   Errors[24].code = 10029;Errors[24].desc = "Order or position frozen";
   Errors[25].code = 10030;Errors[25].desc = "Invalid order filling type";

   // Common Errors
   Errors[26].code = 4001;Errors[26].desc = "Unexpected internal error";
   Errors[27].code = 4002;Errors[27].desc = "Wrong parameter in the inner call of the client terminal function";
   Errors[28].code = 4003;Errors[28].desc = "Wrong parameter when calling the system function";
   Errors[29].code = 4004;Errors[29].desc = "Not enough memory to perform the system function";
   Errors[30].code = 4005;Errors[30].desc = "The structure contains objects of strings and/or dynamic arrays and/or structure of such objects and/or classes";
   Errors[31].code = 4006;Errors[31].desc = "Array of a wrong type, wrong size, or a damaged object of a dynamic array";
   Errors[32].code = 4007;Errors[32].desc = "Not enough memory for the relocation of an array, or an attempt to change the size of a static array";
   Errors[33].code = 4008;Errors[33].desc = "Not enough memory for the relocation of string";
   Errors[34].code = 4009;Errors[34].desc = "Not initialized string";
   Errors[35].code = 4010;Errors[35].desc = "Invalid date and/or time";
   Errors[36].code = 4011;Errors[36].desc = "Requested array size exceeds 2 GB";
   Errors[37].code = 4012;Errors[37].desc = "Wrong pointer";
   Errors[38].code = 4013;Errors[38].desc = "Wrong type of pointer";
   Errors[39].code = 4014;Errors[39].desc = "System function is not allowed to call";

}

¡Por favor, tenga en cuenta que la descripción de la implementación se lleva a cabo fuera de la clase! ¡Solo los métodos se pueden declarar dentro de la clase! No obstante, esto no es obligatorio. Si así lo desea, puede describir el cuerpo de cada método en la clase, pero en mi opinión, es algo inconveniente y difícil de entender.

Como he comentado, solo se declaran titulares de funciones de método en el cuerpo de la clase, mientras que las descripciones se realizan fuera de la clase. Al describir un método, debe especificar a qué clase pertenece. Para ello se usa la operación de permiso de contexto ::. Tal y como se observa en el código de arriba, primero se especifica el tipo de devolución de un método (para un constructor, sería void), después vendría el nombre de la clase (nombre del contexto al que pertenece el método), y al nombre de la clase le seguiría la operación de permiso de contexto. A continuación vendría el nombre del método con sus parámetros de entrada. Después de todo esto comienza la descripción del algoritmo para el método.

Primero se configuran todas las flags del constructor, así como los archivos de sonido y de registro: 

SetAlert(false);
SetPrint(false);
SetSound(false);
SetWriteFlag(false);
SetPlaySoundFile("alert.wav");
SetWritePath("LogErrors.txt"); 

Cada uno de estos métodos funciona con una propiedad de clase determinada. Esto se hace intencionalmente para casos en los que se necesite filtrar valores de propiedades configurados por un usuario. Por ejemplo, puede configurar una determinada máscara que se debería corresponder con un nombre de archivo y una ruta configurados personalmente. Si ahora hay una correspondencia con una máscara, se informará al usuario sobre ello. 

Seguramente habrá notado que todas las flags toman el valor "false". Es decir, no se mantendrán informes por defecto al crear un modelo de clase. Un usuario debe seleccionar qué informes se deben mantener y activarlos usando los mismos métodos "Set" ("Configurar") en la función OnInit(). Igualmente, puede cambiar el nombre y ruta del archivo de registro (la ruta se genera en relación con el directorio 'MetaTrader 5\MQL5\Files\') y el archivo de sonido (la ruta se genera en relación con el directorio 'MetaTrader 5\Sounds\').

Tras configurar todas las flags, inicializamos la variable _CountErrors, asignándole un valor de 150 (la información sobre 149 arrays se almacenará en el array), y después configuraremos el tamaño del array usando la función ArrayResize(). Después de eso, comenzaremos a llenar el array.

Métodos de Configuración de Flag

A la descripción del constructor le sigue la descripción de los métodos de configuración de flag y de configuración de nombres de archivos de sonido y registro:

void ControlErrors::SetAlert(bool value)
{
   _AlertInfo = value;
}

void ControlErrors::SetPrint(bool value)
{
   _PrintInfo = value;
}

void ControlErrors::SetSound(bool value)
{
   _PlaySound = value;
}

void ControlErrors::SetWriteFlag(bool flag)
{
   _WriteFile = flag;
}

void ControlErrors::SetWritePath(string path)
{
   _DataPath = path;
}

void ControlErrors::SetPlaySoundFile(string file)
{
   _PlaySoundFile = file;
}

Como puede ver en el código, es una asignación simple transferida al método de parámetro, propiedad de clase. Las flags no requieren revisión alguna porque solo pueden adoptar dos valores. Sin embargo, los nombres de archivos y rutas se deben revisar antes de asignarse.

Las llamadas a estos métodos, así como a todos los demás, tienen este aspecto: 

type Class_name::Function_Name(parameters_description)
{
   // function body
}

A continuación mostramos la descripción de métodos de trabajo con errores, y los primeros son mGetLastError() y mResetLastError().

Métodos mGetLastError() y mResetLastError() 

El nombre de método mGetLastError() lo dice todo. Duplica la función GetLastError(). Pero además de la llamada a GetLastError(), se busca una descripción para el código de error obtenido en el array _Errors, y los detalles del error (el código y su descripción) se guardan en la variable _Error para que en los siguientes usos se utilice el valor guardado, en lugar de llamar a GetLastError() en cada ocasión.

El código de método:

int ControlErrors::mGetLastError(void)
{
   _Error.code = GetLastError();
   _Error.desc = mGetDesc(_Error.code);
   return _Error.code;
}

El método mResetLastError() duplica la función ResetLastError():

void ControlErrors::mResetLastError(void)
{
   ResetLastError();
}

Métodos para Trabajar con el Mensaje de Último Error

Son dos métodos: mGetError() y mResetError().

El método mGetError() devuelve el código contenido en _Error.code:

int ControlErrors::mGetError(void)
{
   return _Error.code;
}

El método mResetError() restablece los contenidos de la variable _Error:

void ControlErrors::mResetError(void)
{
   _Error.code = 0;
   _Error.desc = "";
}

Método para Determinar Tipo de Error mGetTypeError()

El siguiente método es mGetTypeError(). Comprueba si el último error ocurrido es uno personal, o si está predefinido (es decir, que está contenido en el array _Errors).

El código de método:

int ControlErrors::mGetTypeError(void)
{
   if (mGetError() < ERR_USER_ERROR_FIRST)
   {
      return 0;
   }
   else if (mGetError() >= ERR_USER_ERROR_FIRST)
   {
      return 1;
   }
   return -1;
}

La constante ERR_USER_ERROR_FIRST tiene el valor 65536. A partir de estos códigos comienzan los errores personalizados. De modo que en el cuerpo del método se revisa el último código de error recibido. Si el método devuelve cero, este es un error predefinido. Si devuelve uno, es un error personalizado.

Métodos para Trabajar con Errores Personalizados

En MQL5, los usuarios pueden configurar sus propios errores en el transcurso del programa. Para que se puedan asignar descripciones apropiadas a los códigos personalizados, la propiedad _UserError está disponible en la clase. Para trabajar con esta propiedad se usan dos métodos.

El método mSetUserError() se usa para configurar un código y describir el error personalizado:

void ControlErrors::mSetUserError(ushort value, string desc = "")
{
   SetUserError(value);
   _UserError.code = value;
   _UserError.desc = desc;
}

Primero, la función SetUserError() configura la variable predefinida _LastError como un valor igual al valor ERR_USER_ERROR_FIRST. Y después el valor y su descripción asignada se guardan en la variable _UserError.

El segundo método mResetUserError() restablece los campos de la variable _UserError:

void ControlErrors::mResetUserError(void)
{
   _UserError.code = 0;
   _UserError.desc = "";
}

Este método solo funciona con la variable _UserError. Para restablecer el valor de la variable de sistema _LastError se usa otro método: mResetLastError(), que ya describimos arriba.

Método para Obtener Descripción de Error de Código

Hay también un método especial mGetDesc() en la clase que, al llamarse, devolverá la descripción del código de error del array Errors, o del campo desc de la variable _UserError, si el error se compuso por un usuario:

string ControlErrors::mGetDesc(int nErr=0)
{
   int ErrorNumber = 0;
   string ReturnDesc = "";
   
   ErrorNumber = (mGetError()>0)?mGetError():ErrorNumber;
   ErrorNumber = (nErr>0)?nErr:ErrorNumber;
   
   if ((ErrorNumber > 0) && (ErrorNumber < ERR_USER_ERROR_FIRST))
   {
      for (int i = 0;i<_CountErrors;i++)
      {
         if (Errors[i].code == ErrorNumber)
         {
            ReturnDesc = Errors[i].desc;
            break;
         }
      }
   }
   else if (ErrorNumber > ERR_USER_ERROR_FIRST)
   {
      ReturnDesc = (_UserError.desc=="")?"Cusrom error":_UserError.desc;
   }
      
   if (ReturnDesc == ""){return "Unknown error code: "+(string)ErrorNumber;}
   return ReturnDesc;
}

Este método tiene un parámetro nErr. Por defecto, es igual a cero. Si durante la llamada al método se configura un valor al parámetro, se buscará la descripción para el valor establecido. Si no se configura el parámetro, no se buscará la descripción para el último código de error recibido.

Al principio se declaran dos variables en el método: ErrorNumber - con esta variable se procesa el código de error; y ReturnDesc - en ella se almacenará la descripción obtenida de ErrorNumber. En las dos líneas siguientes, al asignar un valor a ErrorNumber, se usará el operador condicional ?:. Este es un análogo simplificado de la construcción if-else.

ErrorNumber = (mGetError()>0)?mGetError():ErrorNumber;
ErrorNumber = (nErr>0)?nErr:ErrorNumber;

En la primera línea, definimos si un error se corrigió, es decir, si mGetError() devolvió un resultado que no fuera cero; después, el código de error obtenido (el valor devuelto por el método mGetError()) se asignará a la variable ErrorNumber; de lo contrario, se asignará el valor de la variable ErrorNumber. En la segunda línea se realiza la misma comprobación, pero para el parámetro del método mGetError(). Si el valor de nErr no es cero, se asigna a la variable ErrorNumber.

Una vez que recibimos el código de error, comenzaremos a buscar descripciones para este código. Si el código obtenido es mayor que cero y menor que ERR_USER_ERROR_FIRST, es decir, que no es un error personalizado, buscaremos su descripción en un bucle. Y si el código obtenido es mayor que ERR_USER_ERROR_FIRST, tomaremos la descripción del campo desc de la variable _UserError.

Al final de la revisión, comprobamos si se encontró la descripción. Si no, se devolverá un mensaje sobre un código de error desconocido.

Métodos de Señal

Entre los métodos de señal se incluyen mAlert(), mPrint() y mSound(). Estos métodos son muy parecidos en su distribución:

void ControlErrors::mAlert(string message="")
{
   if (_AlertInfo == true)
   {
      if (message == "")
      {
         if (mGetError() > 0)
         {
            Alert("Error №",mGetError()," - ",mGetDesc());
         }
      }
      else
      {
         Alert(message);
      }   
   }
}

void ControlErrors::mPrint(string message="")
{
   if (_PrintInfo == true)
   {
      if (message == "")
      {
         if (mGetError() > 0)
         {
            Print("Error №",mGetError()," - ",mGetDesc());
         }
      }
      else
      {
         Print(message);
      }
   }
}

void ControlErrors::mSound(void)
{
   if (_PlaySound == true)
   {
      PlaySound(_PlaySoundFile);
   }
}

En los tres métodos, al principio se revisa la flag que permite informes y señales. Después, en los métodos mAlert() y mPrint(),el parámetro de entrada message (mensaje) se revisa para comprobar el mensaje que se debería mostrar en el cuadro de diálogo de Alerta o añadir al diario. Si se envía un mensaje en message y el último código de error es mayor que cero, entonces se mostrará. Si no, se podrá ver un mensaje estándar. El método mSound() no tiene parámetros, de modo que tras revisar la flag, la función PlaySound() se llama inmediatamente para producir un sonido.

Método Check()

Este método simplemente llama a todas las funciones de esta clase en la secuencia correcta, por tanto se revisa la ocurrencia de nuevos errores, se publican todos los informes permitidos, e inmediatamente después de eso se elimina el código de error con su descripción. Así, el método Check() realiza una revisión completa.

int ControlErrors::Check(string st="")
{
   int errNum = 0;
   errNum = mGetLastError();
   mFileWrite();
   mAlert(st);
   mPrint(st);
   mSound();
   mResetError();
   mResetLastError();
   mResetUserError();
   return errNum;
}

Check() tiene un parámetro de tipo cadena de caracteres. Este es un mensaje personalizado que pasa de los métodos mAlert() y mPrint() to a escribirse en informes.

Métodos para Escribir Mensajes a un Archivo de Registro

Este método se llama mFileWrite(). Si se permite mantener un archivo de registro y se especifica la ruta al archivo correctamente, este método graba la información en el archivo especificado.

int ControlErrors::mFileWrite(string message = "")
{
   int      handle  = 0,
            _return = 0;
   datetime time    = TimeCurrent();
   string   text    = (message != "")?message:time+" - Error №"+mGetError()+" "+mGetDesc();
   
   if (_WriteFile == true)
   {
      handle = FileOpen(_DataPath,FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
      if (handle != INVALID_HANDLE)
      {
         ulong size = FileSize(handle);
         FileSeek(handle,size,SEEK_SET);
         _return = FileWrite(handle,text);
         FileClose(handle);
      }
   }
   return _return;
}

Al principio se declaran cuatro variables: handle - para almacenar el identificador de un archivo abierto, _return - para almacenar el valor devuelto, time - para mantener la fecha actual (fecha de grabación al archivo) y text - el mensaje de texto que se escribirá al archivo. El método mFileWrite() tiene un parámetro de entrada - message, con el que el usuario puede pasar cualquier cadena de caracteres que se debe escribir al archivo.

Esta herramienta se puede usar para grabar valores de indicador, precios y otros datos requeridos en determinados momentos.

Tras declarar las variables, se comprueba la flag _WriteFile. Y si se permite el mantenimiento de un archivo de registro, el archivo se abrirá para su reescritura usando la función FileOpen(). El primer parámetro de FileOpen() es la propiedad DataPath, que contiene su nombre de archivo y ruta. El segundo parámetro es un conjunto de flags que determina el modo de trabajar con flags. En nuestro caso, se usarán cuatro flags:

En el siguiente paso, comprobaremos si el archivo se abrió con éxito o no. Si no, el identificador tendrá el valor INVALID_HANDLE, y la operación de método acabará aquí. Pero si tuvo éxito, obtendremos el tamaño del archivo usando FileSize(), y después, usando FileSeek(), moveremos la posición del puntero del archivo al final del archivo y añadiremos mensajes al final del archivo usando la función FileWrite(). Tras todo esto, cerraremos el archivo con la función FileClose().

Pasaremos el identificador del archivo cuyo tamaño debemos devolver como un parámetro de entrada a la función FileSize(). Este es el único parámetro de esta función.

Se deben especificar tres parámetros para la operación de FileSeek():

Se requieren al menos dos parámetros para el trabajo de la función FileWrite(). Este es el identificador de un archivo al que debemos escribir datos de texto. El segundo es una línea de texto que se debe escribir, y todas las líneas de texto siguientes se escribirán al archivo. El número de parámetros no debe superar los 63.

La función FileClose() también necesita el identificador del archivo para cerrarlo.

Ejemplos

Ahora me gustaría añadir unos pocos ejemplos frecuentes de uso de las clases que hemos escrito. Empecemos con la creación de objetos y permitamos el mantenimiento de los informes necesarios.

Creemos un objeto de clase:

#include <ControlErrors.mqh>

ControlErrors mControl;

Antes de crear un objeto, debemos añadir el archivo que contiene la descripción de clase al Asesor Experto. Esto se hace al principio del programa a través de la directiva #include. Y solo después de que se haya creado el objeto tendrá el mismo aspecto que si hubiéramos creado una variable nueva. Pero en lugar del tipo de datos, se inserta el nombre de la clase. 

Ahora creemos informes ettor que deseamos recibir. Esto se hace en la función OnInit(): 

int OnInit()
{
//---
mControl.SetAlert(true);
mControl.SetPrint(true);
mControl.SetSound(false);
mControl.SetWriteFlag(true);
mControl.SetPlaySoundFile("news.wav");
//---
return(0);
}

Por defecto, cuando se crea un objeto, todas las flags de permiso se establecen como "false", es decir, todos los informes están desactivados. Por eso, en OnInit() no se requiere llamar a métodos con el valor "false", porque se hace en el ejemplo de arriba (método SetSound()). Estos métodos también se pueden llamar en otras partes del programa. Por ejemplo, si necesita desactivar el mantenimiento de informes bajo determinadas condiciones, puede programar estas condiciones y configurar flags para los valores requeridos cuando se cumplen las condiciones.

Otra cosa que debemos mencionar aquí es la llamada de métodos durante la ejecución del programa y "caza" de errores. Esta parte no es difícil, porque aquí puede usar el método sencillo Check(), configurando todas las flags antes:

mControl.Check();

Este método, como se dijo arriba, identificará el código del error ocurrido, llamará todos los métodos que mantienen informes y después restablecerá los valores de todas las variables que contienen información sobre el último error. Si la forma de procesamiento de error ofrecida por Check() no le gusta por algún motivo, puede generar sus propios informes usando la variable "class methods" (métodos de clase).