
Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 2): Añadir controles y capacidad de respuesta
Introducción
En nuestro artículo anterior, sentamos las bases montando los elementos gráficos de nuestro panel de interfaz gráfica de usuario (GUI) MetaQuotes Language 5 (MQL5). Si recuerdas, la iteración fue un ensamblaje estático de elementos GUI: una mera instantánea congelada en el tiempo, sin capacidad de respuesta. Era estático e inflexible. Ahora, descongelemos esa instantánea e infundámosle vida. En esta continuación tan esperada, llevaremos nuestro panel al siguiente nivel. Abróchese el cinturón mientras exploramos cómo darle vida a nuestra interfaz:
- Diseño y capacidad de respuesta: ¡Olvídese de los componentes estáticos! Adoptaremos un posicionamiento relativo, diseños flexibles y componentes editables, adaptables y sobre los que se puede hacer clic para que nuestro panel responda a las interacciones del usuario.
- Actualizaciones dinámicas: Los datos en tiempo real son el latido de cualquier aplicación de trading. Nos sumergiremos en la obtención de precios en directo y nos aseguraremos de que nuestro panel refleje la información más reciente del mercado.
- Movilidad de los componentes: Imagine elementos arrastrables: paneles que responden al tacto del usuario. Exploraremos cómo hacer que ciertos componentes sean móviles, mejorando la experiencia del usuario.
Los siguientes temas nos orientarán sobre cómo conseguir un panel interactivo y con capacidad de respuesta:
- Ilustración de los elementos que deben automatizarse
- Automatización de la GUI en MQL5
- Conclusión
Ilustración de los elementos que deben automatizarse
Se van a automatizar siete componentes. El primer componente es el cierre del panel cuando se hace clic en el botón de cierre. Pretendemos eliminar todos los elementos del panel cuando se haga clic en este botón. En segundo lugar, cuando se hace clic en los botones de gestión de posiciones, estos cerrarán sus respectivas posiciones y órdenes según las instrucciones. Por ejemplo, cuando hacemos clic en el botón o etiqueta "Profit", cerramos todas las posiciones que solo tienen ganancias. La tercera automatización se centrará en el componente de volumen de operaciones. Una vez que se hace clic en la entidad, se creará una lista desplegable de opciones para que el usuario elija una opción comercial.
La cuarta automatización estará en los botones de aumento o disminución junto a los respectivos botones de negociación para incrementar o disminuir los valores en los campos de edición, en lugar de simplemente escribirlos. En caso de que el usuario quiera ingresar los valores deseados directamente, el campo de edición deberá capturar los valores ingresados, y este es nuestro quinto paso de automatización. Luego, el sexto paso será la creación de un efecto de desplazamiento sobre el botón desplazado. Es decir, cuando el mouse está dentro del área del botón sobre el que se desplaza el cursor, el botón crecerá indicando que el mouse está cerca del botón, y cuando el mouse se aleja del área del botón, restablecerá el botón a sus funciones predeterminadas. Finalmente, actualizaremos las cotizaciones de precios a valores en tiempo real en cada variación de precio.
Para ayudar a comprender fácilmente estos procesos y componentes de automatización, a continuación se presenta una descripción detallada de ellos presentando el hito anterior.
Con la idea de lo que haremos, comencemos la automatización de inmediato. Por favor, consulta el artículo anterior donde creamos el ensamblaje estático de los elementos de la GUI si aún no has pasado por él para que vayas bien encaminado con nosotros. Vamos a hacerlo.
Automatización de la GUI en MQL5
Iremos de procesos simples a complejos para que nuestra estructura quede ordenada cronológicamente. De esta forma, actualizaremos los precios en cada tick o cambio de cotización. Para conseguirlo, necesitaremos el manejador de eventos OnTick, una función incorporada en MQL5 que se suele llamar cuando cambian las cotizaciones de los precios. La función es un tipo de datos void, lo que significa que maneja las ejecuciones directamente y no tiene que devolver ninguna salida. Su función debería parecerse a la siguiente:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ ... } //+------------------------------------------------------------------+
Este es el manejador de eventos que es responsable de las actualizaciones de precios y por lo tanto el corazón de nuestra lógica. Añadiremos la lógica de control a esta función como se indica a continuación:
// --- Update price quotes --- // Set the text of the "SELL PRICE" label to the current Bid price ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, Bid());
Usamos ObjectSetString para establecer la propiedad del objeto, text en este caso, ya que necesitamos alterar a entrada de texto de la etiqueta del botón. Proporcionamos el ID del gráfico de la ventana como 0 para el gráfico actual, o también podría proporcionar «ChartID()», una función que proporcionará el índice de identificación del gráfico para la ventana del gráfico actual. A continuación, proporcionamos «LABEL_SELL_PRICE» como nombre del objeto de destino para actualizar la etiqueta del botón de venta, y «OBJPROP_TEXT», para indicar que la propiedad del objeto que estamos actualizando es el valor string de texto del objeto. Finalmente, proporcionamos el valor del texto. El precio de la oferta es el valor que necesitamos actualizar, y por eso lo completamos, pero eso no es todo. El tipo de propiedad que necesitamos completar es un valor de datos string, y nuestro precio de oferta está en formato doble. Por lo tanto, necesitamos convertir el valor de tipo doble en un valor de tipo string; de lo contrario, recibiremos una advertencia durante la compilación: implicit conversion from 'number' to 'string'.
En este punto, podríamos convertir el valor doble a string directamente como se muestra a continuación, pero eso generalmente no se recomienda ya que debe usarse con cuidado.
// --- Update price quotes --- // Set the text of the "SELL PRICE" label to the current Bid price ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, (string)Bid());
La conversión de tipos en MQL5, convertir un valor numérico en string, tiene algunos matices. Una de las más comunes es la pérdida de precisión. Por ejemplo, nuestro valor de precio de oferta es un valor de coma flotante y, por ejemplo, cuando su precio sea 11,77900, se ignorarán los dos últimos ceros y el valor final de salida será 11,779. Técnicamente, no hay ninguna diferencia lógica entre los dos valores, pero visualmente, hay una diferencia matemática en que uno contiene 5 dígitos y el otro tiene 3 dígitos. He aquí un ejemplo de lo que queremos decir.
Como hemos visto, la conversión de tipos eliminará la advertencia, pero no es el mejor método cuando la precisión es importante. Por lo tanto, se necesitará otra función. Utilizamos la función DoubleToString incorporada en MQL5 para realizar la conversión. Esta función se utiliza para convertir un valor numérico con coma flotante en un string de texto. Toma dos parámetros o argumentos de entrada, el valor objetivo en coma flotante y el formato de precisión. En nuestro caso, utilizamos el precio de oferta como valor objetivo y el formato de precisión como _Digits, una variable que almacena el número de dígitos después de un punto decimal, que define la precisión del precio del símbolo del gráfico actual. También puede utilizar la función Digits(). Este sería cualquier número arbitrario dentro del rango de 0 a 8, y si se omite asumiría un valor de 8 dígitos. Por ejemplo, nuestro símbolo es el ORO, (XAUUSD), con 3 dígitos. Así que tendríamos 3 como nuestro valor de dígitos, pero para la automatización y para hacer el código adaptable a los pares de divisas, utilizamos la función para recuperar automáticamente el número de dígitos del par de divisas en particular. Sin embargo, si desea un rango fijo de decimales, utilice un valor estático. Aquí está el código final para este ajuste del precio de oferta.
// --- Update price quotes --- // Set the text of the "SELL PRICE" label to the current Bid price ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits));
Como ahora tenemos la lógica de conversión correcta, gracias a los desarrolladores de MQL5 por la hermosa función, tendremos los resultados a continuación.
Para fijar el precio de venta del botón de compra y el diferencial, prevalece la misma lógica. Aquí está el código para ello.
// --- Update price quotes --- // Set the text of the "SELL PRICE" label to the current Bid price ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits)); // Set the text of the "BUY PRICE" label to the current Ask price ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits)); // Set the text of the "SPREAD" button to the current spread value ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, (string)Spread());
Deberías haber notado que para la dispersión escribimos directamente su valor string a pesar de que antes criticamos ese enfoque para mantener la precisión. Aquí, la función de dispersión es un tipo de dato entero, y por lo tanto la precisión no es de máxima prioridad, de cualquier manera, tendremos el formato correcto. Sin embargo, también podría utilizar la función IntegerToString para realizar la conversión, lo que daría como resultado el mismo valor.
// Set the text of the "SPREAD" button to the current spread value ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));
La función toma tres argumentos, pero sólo basta con el valor objetivo, ya que no especifica el formato de precisión. Ahora puedes ver la diferencia. En un formato de intercambio gráfico (Graphic Interchange Format, GIF), esto es lo que hemos conseguido actualmente.
Eso es todo lo que tenemos que hacer en el manejador de eventos y el código fuente completo que es responsable de la actualización de los precios es el siguiente:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ // --- Update price quotes --- // Set the text of the "SELL PRICE" label to the current Bid price ObjectSetString(0, LABEL_SELL_PRICE, OBJPROP_TEXT, DoubleToString(Bid(), _Digits)); // Set the text of the "BUY PRICE" label to the current Ask price ObjectSetString(0, LABEL_BUY_PRICE, OBJPROP_TEXT, DoubleToString(Ask(), _Digits)); // Set the text of the "SPREAD" button to the current spread value ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread())); } //+------------------------------------------------------------------+
Ahora, el primer componente de automatización está hecho. Ha sido fácil, ¿verdad? A continuación, pasamos a los demás componentes de nuestro panel GUI. La automatización del resto de elementos se realizará dentro del manejador de funciones OnChartEvent, por lo que vamos a ver en profundidad sus parámetros de entrada así como sus funciones.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { ... }
El propósito de la función es manejar los cambios en el gráfico realizados por un usuario o por un programa MQL5. De este modo, las interacciones que el usuario realice, como mover el ratón, editar los campos de los botones y hacer clic en etiquetas y botones, serán capturadas y gestionadas por este controlador de eventos. Desglosemos sus argumentos para interpretarlos mejor:
- id: Este parámetro representa el ID del evento y corresponde a uno de los 11 tipos de eventos predefinidos. Entre ellos se incluyen eventos como pulsaciones de teclas, movimientos del ratón, creación de objetos, cambios en los gráficos y eventos personalizados. Para eventos personalizados, puede utilizar IDs desde CHARTEVENT_CUSTOM hasta CHARTEVENT_CUSTOM_LAST. Los 11 tipos de eventos son los que se muestran a continuación:
- lparam: Un parámetro de evento de tipo long. Su valor depende del evento específico que se esté gestionando. Por ejemplo, podría representar un código de tecla durante un evento de pulsación de tecla.
- dparam: Un parámetro de evento de tipo double. Similar a lparam, su valor varía en función del tipo de evento. Por ejemplo, durante un evento de movimiento del ratón, podría transmitir la posición del cursor del ratón.
- sparam: Un parámetro de evento de tipo string. Una vez más, su significado depende del acontecimiento. Por ejemplo, durante la creación de un objeto, podría contener el nombre del objeto recién creado.
Para mostrar esto de forma más comprensible, dentro de la función, tengamos una impresión que contenga los cuatro argumentos del diario.
// Print the 4 function parameters Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);
Esta función imprimirá el ID del evento del gráfico, su valor de evento de tipo long, valor de evento de tipo double y el valor de tipo string. Veamos el siguiente GIF para facilitar las referencias.
A partir del GIF proporcionado, todo debería estar claro. Ahora pasamos a capturar los eventos de clic del gráfico en los elementos del panel GUI. Así, nuestro ID será «CHARTEVENT_OBJECT_CLICK».
//Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam); if (id==CHARTEVENT_OBJECT_CLICK){ ... }
Primero comentamos la línea de código anterior porque no queremos llenar nuestro diario con información irrelevante. Las dos barras (//) utilizadas se denominan comentarios de una sola línea y comentan un código desde su inicio y continúan hasta el final de la línea, de ahí su nombre de comentario de 'una sola línea'. Los comentarios son particularmente ignorados por el ordenador durante la ejecución. Utilizamos la sentencia if para comprobar si se ha producido un clic en el objeto. Esto se consigue equiparando el ID del evento gráfico a las enumeraciones de clic del objeto. Si hacemos clic en un objeto, imprimamos los argumentos y veamos qué obtenemos. Se utiliza el siguiente código.
if (id==CHARTEVENT_OBJECT_CLICK){ Print("ID = ",id,", LM = ",lparam,", DM = ",dparam,", SPARAM = ",sparam); ... }
En la función de impresión, simplemente cambiamos "LPARAM" a "LP" y "DPARAM" a "DP" para que solo podamos concentrarnos en el ID del evento del gráfico y el nombre del objeto en el que se hizo clic, desde allí obtendremos el ID del objeto y tomaremos medidas si es necesario. A continuación se muestra una ilustración de la lógica:
La primera función de automatización del componente será la destrucción del panel GUI cuando se haga clic en el ícono de la ambulancia. En el GIF anterior, puedes ver que una vez que se hace clic en un objeto, el nombre del objeto se almacena en la variable de tipo string. Así, desde esta variable, podemos obtener el nombre del objeto clicado y comprobar si es el objeto deseado y si es así, podemos tomar acción, en nuestro caso destruir el panel.
//--- if icon car is clicked, destroy the panel if (sparam==ICON_CAR){ Print("BTN CAR CLICKED. DESTROY PANEL NOW"); destroyPanel(); ChartRedraw(0); }
Se utiliza otra declaración if para verificar la instancia en la que se hizo clic en el ícono del automóvil y, si ese es el caso, informamos la instancia en la que se hizo clic y se puede realizar la destrucción del panel ya que es el ícono correcto para el trabajo. Luego llamamos a la función “destroyPanel”, cuyo objetivo es eliminar todos los elementos de nuestro panel. Esta función ya debería resultarte familiar, ya que la usamos en nuestro artículo anterior, que es la parte 1. Por último, llamamos a la función ChartRedraw. La función se utiliza para forzar el rediseño de un gráfico específico. Al modificar propiedades u objetos de un gráfico (como indicadores, líneas o formas) mediante programación, es posible que los cambios no se reflejen inmediatamente en el gráfico. Al llamarlo, se asegura de que el gráfico se actualice y muestre los últimos cambios. En una representación visual, aquí están los resultados que obtenemos.
Como puedes ver, la lógica es bastante simple. Se debe emplear el mismo método en los demás clics de objetos. Pasemos ahora al evento cuando se hace clic en la etiqueta del botón de cierre. Cuando esto sucede, debemos cerrar todas las posiciones abiertas y eliminar todas las órdenes pendientes. Esto garantizará que no tengamos ninguna orden de mercado. Se necesitará una declaración else-if para verificar la condición de si se hizo clic en el botón cerrar.
else if (sparam == BTN_CLOSE) { // Button "Close" clicked. Close all orders and positions now. Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW"); // Store the original color of the button long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR); // Change the button color to red (for visual feedback) ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed); ... }
Aquí queremos agregar un pequeño ajuste a la instancia del evento. Queremos que cuando se haga clic en el botón, cambiemos el color del mismo indicando que se hizo clic en el botón para que se inicie el proceso para cerrar las órdenes de mercado. Después del cierre completo, necesitaremos restablecer el color del botón a su propiedad de color predeterminada. Para obtener el color original de la etiqueta del botón, declaramos una variable de tipo de datos largo llamada "originalColor" y en ella almacenamos el color predeterminado del botón. Para recuperar el color del botón, utilizamos la función ObjectGetInteger, pasamos el ID del gráfico, el nombre del botón y la propiedad del botón, color en nuestro caso. Después de almacenar el color original, ahora podemos manipular el color de la etiqueta del botón ya que tenemos una reserva de su valor original. Utilizamos ObjectSetInteger para establecer el color del objeto en rojo. Estando en ese estado, iniciamos el proceso de cierre del pedido.
// Iterate through all open positions for (int i = 0; i <= PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionSelectByTicket(ticket)) { // Check if the position symbol matches the current chart symbol if (PositionGetString(POSITION_SYMBOL) == _Symbol) { obj_Trade.PositionClose(ticket); // Close the position } } } }
Utilizamos un bucle for para iterar por todas las posiciones abiertas y cerrarlas. Para obtener todas las posiciones abiertas, utilizamos una función MQL5 incorporada PositionsTotal. Esta función devolverá el número de posiciones abiertas en esa cuenta de negociación concreta. A continuación obtenemos el ticket de esa posición proporcionando el índice de la posición en la función PositionGetTicket y lo almacenamos en la variable de tipo de datos ulong llamada «ticket». La función devuelve el ticket de la posición especificada y en caso de fallo, devuelve 0. Para continuar, debemos asegurarnos de que tenemos un ticket. Esto se consigue utilizando la sentencia if para asegurarse de que el valor del ticket es mayor que 0. Si es así, significa que tenemos un ticket y seguimos seleccionando el ticket para poder trabajar con él. Si seleccionamos correctamente el billete, podemos recuperar la información del puesto. Dado que podría haber un par de posiciones en esa cuenta de negociación en particular, nos aseguramos de que sólo cerramos las posiciones asociadas con ese par de divisas en particular. Finalmente, cerramos esa posición por el número de ticket y procedemos a hacer lo mismo con otras posiciones abiertas si las hubiera.
Sin embargo, para cerrar la posición utilizamos «obj_Trade» seguido de un operador de punto. Esto se denomina objeto de clase. Para realizar fácilmente la operación de cierre de posición, necesitamos incluir una instancia de clase que ayude en el proceso. Así, incluimos una instancia de comercio utilizando #include al principio del código fuente. Esto nos da acceso a la clase CTrade, que utilizamos para crear un objeto comercial. Esto es crucial, ya que lo necesitamos para realizar las operaciones comerciales.
#include <Trade/Trade.mqh>
CTrade obj_Trade;
El preprocesador sustituirá la línea #include <Trade/Trade.mqh> por el contenido del archivo Trade.mqh. Los corchetes indican que el archivo Trade.mqh se tomará del directorio estándar (normalmente es directorio_de_instalación_del_terminal\MQL5\Include). El directorio actual no se incluye en la búsqueda. La línea puede colocarse en cualquier parte del programa, pero normalmente, todas las inclusiones se colocan al principio del código fuente, para una mejor estructura del código y una referencia más fácil. La declaración del objeto obj_Trade de la clase CTrade nos dará acceso a los métodos contenidos en dicha clase fácilmente, gracias a los desarrolladores de MQL5.
Para eliminar las órdenes pendientes, se utiliza la misma lógica de iteración.
// Iterate through all pending orders for (int i = 0; i <= OrdersTotal(); i++) { ulong ticket = OrderGetTicket(i); if (ticket > 0) { if (OrderSelect(ticket)) { // Check if the order symbol matches the current chart symbol if (OrderGetString(ORDER_SYMBOL) == _Symbol) { obj_Trade.OrderDelete(ticket); // Delete the order } } } }
La mayor diferencia en la lógica de iteración es que utilizamos la función OrdersTotal para obtener el total de pedidos. Todo lo demás está vinculado a los pedidos. Una vez cerradas todas las posiciones y eliminadas las órdenes, tendremos que restablecer el color original de la etiqueta del botón.
// Reset the button color to its original value Print("Resetting button to original color"); ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor); // Force a redrawing of the chart to reflect the changes ChartRedraw(0);
La función «ObjectSetInteger» se utiliza pasando el ID del gráfico, el nombre del botón, la propiedad color y el color original. Aquí es donde nuestra variable anterior resulta muy útil. No tenemos que almacenar siempre el color original de un objeto, sino que podemos almacenarlo y recuperarlo automáticamente. El código completo responsable de cerrar todas las posiciones abiertas y eliminar todas las órdenes abiertas es el siguiente:
else if (sparam == BTN_CLOSE) { // Button "Close" clicked. Close all orders and positions now. Print("BTN CLOSE CLICKED. CLOSE ALL ORDERS & POSITIONS NOW"); // Store the original color of the button long originalColor = ObjectGetInteger(0, BTN_CLOSE, OBJPROP_COLOR); // Change the button color to red (for visual feedback) ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, clrRed); // Iterate through all open positions for (int i = 0; i <= PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionSelectByTicket(ticket)) { // Check if the position symbol matches the current chart symbol if (PositionGetString(POSITION_SYMBOL) == _Symbol) { obj_Trade.PositionClose(ticket); // Close the position } } } } // Iterate through all pending orders for (int i = 0; i <= OrdersTotal(); i++) { ulong ticket = OrderGetTicket(i); if (ticket > 0) { if (OrderSelect(ticket)) { // Check if the order symbol matches the current chart symbol if (OrderGetString(ORDER_SYMBOL) == _Symbol) { obj_Trade.OrderDelete(ticket); // Delete the order } } } } // Reset the button color to its original value Print("Resetting button to original color"); ObjectSetInteger(0, BTN_CLOSE, OBJPROP_COLOR, originalColor); // Force a redrawing of the chart to reflect the changes ChartRedraw(0); }
Siempre se recomienda que después de cada adición de lógica a un panel, compilar y ejecutar el código para asegurarse de que todo está funcionando según lo previsto antes de la graduación a otra lógica de control. Esto es lo que hemos conseguido hasta ahora.
Ahora podemos cerrar todas las posiciones y órdenes con éxito. Observe cómo al hacer clic en el botón de cierre, mientras se cierran las posiciones, el color de la etiqueta del botón permanece rojo hasta que se cierran todas y finalmente retoma su color original. De nuevo, puede observar que no cerramos la posición de compra «AUDUSD» porque el Asesor Experto (EA) está actualmente unido al símbolo Oro. Ahora se puede utilizar la misma lógica para establecer la capacidad de respuesta de las etiquetas de los otros botones.
else if (sparam == BTN_MARKET) { // Button "Market" clicked. Close all positions related to the current chart symbol. Print(sparam + " CLICKED. CLOSE ALL POSITIONS NOW"); // Iterate through all open positions for (int i = 0; i <= PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionSelectByTicket(ticket)) { // Check if the position symbol matches the current chart symbol if (PositionGetString(POSITION_SYMBOL) == _Symbol) { obj_Trade.PositionClose(ticket); // Close the position } } } } // Force a redrawing of the chart to reflect the changes ChartRedraw(0); }
La diferencia de este código con el código del botón cerrar es que nos deshacemos de la iteración de cierre de la orden, ya que sólo queremos cerrar todas las posiciones abiertas. Para cerrar todas las posiciones con beneficios, se emplea el siguiente fragmento de código.
else if (sparam == BTN_PROFIT) { // Button "Profit" clicked. Close all positions in profit now. Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN PROFIT NOW"); // Iterate through all open positions for (int i = 0; i <= PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionSelectByTicket(ticket)) { // Check if the position symbol matches the current chart symbol if (PositionGetString(POSITION_SYMBOL) == _Symbol) { double profit_or_loss = PositionGetDouble(POSITION_PROFIT); if (profit_or_loss > 0) { obj_Trade.PositionClose(ticket); // Close the position } } } } } // Force a redrawing of the chart to reflect the changes ChartRedraw(0); }
La principal diferencia de este fragmento de código con respecto al anterior, que se supone que cierra todas las posiciones abiertas, es que añadimos una lógica adicional para comprobar si el beneficio de la posición es superior a cero, lo que significa que sólo cerramos las posiciones que están en beneficios. A continuación se expone la lógica específica:
double profit_or_loss = PositionGetDouble(POSITION_PROFIT); if (profit_or_loss > 0) { obj_Trade.PositionClose(ticket); // Close the position }
Definimos una variable de tipo de datos doble llamada «profit_or_loss» y en ella almacenamos la ganancia o pérdida flotante actual de la posición seleccionada. Si el valor es mayor que 0, cerramos la posición puesto que ya está en beneficios. La misma lógica se traslada al botón de pérdidas como se indica a continuación, donde sólo cerramos una posición si está en pérdidas.
else if (sparam == BTN_LOSS) { // Button "Loss" clicked. Close all positions in loss now. Print(sparam + " CLICKED. CLOSE ALL POSITIONS IN LOSS NOW"); // Iterate through all open positions for (int i = 0; i <= PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket > 0) { if (PositionSelectByTicket(ticket)) { // Check if the position symbol matches the current chart symbol if (PositionGetString(POSITION_SYMBOL) == _Symbol) { double profit_or_loss = PositionGetDouble(POSITION_PROFIT); if (profit_or_loss < 0) { obj_Trade.PositionClose(ticket); // Close the position } } } } } // Force a redrawing of the chart to reflect the changes ChartRedraw(0); }
Por último, para cerrar las órdenes pendientes cuando se pulsa la etiqueta del botón pendiente, se utiliza la iteración de órdenes y su código es el siguiente.
else if (sparam == BTN_PENDING) { // Button "Pending" clicked. Delete all pending orders related to the current chart symbol. Print(sparam + " CLICKED. DELETE ALL PENDING ORDERS NOW"); // Iterate through all pending orders for (int i = 0; i <= OrdersTotal(); i++) { ulong ticket = OrderGetTicket(i); if (ticket > 0) { if (OrderSelect(ticket)) { // Check if the order symbol matches the current chart symbol if (OrderGetString(ORDER_SYMBOL) == _Symbol) { obj_Trade.OrderDelete(ticket); // Delete the order } } } } // Force a redrawing of the chart to reflect the changes ChartRedraw(0); }
A continuación se muestra la visualización de los hitos.
Como se ilustra, es evidente que los botones de la cabecera de nuestro panel ahora responden cuando se hace clic en ellos. Ahora pasamos a darle vida al botón de volumen de operaciones. Queremos que cuando se haga clic en el botón o en la propia etiqueta o cuando se haga clic en el icono desplegable, creemos otro subpanel con las distintas listas de opciones que el usuario puede elegir. A continuación se expone la lógica:
else if (sparam == BTN_LOTS || sparam == LABEL_LOTS || sparam == ICON_DROP_DN1) { // Button "Lots," label "Lots," or dropdown icon clicked. Create a dropdown list. Print(sparam + " CLICKED. CREATE A DROPDOWN LIST"); // Enable the button for dropdown functionality ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, true); // Create the dropdown list createDropDown(); // Redraw the chart to reflect the changes ChartRedraw(0); }
Una vez pulsado el botón, informamos de la instancia y establecemos el estado del botón en true. Esto hace que el botón se vuelva más oscuro, lo que indica que se ha hecho clic en él. Una vez hecho esto, creamos la lista desplegable llamando a la función personalizada «createDropDown», cuyo fragmento de código se proporcionó anteriormente en el primer artículo. Una vez realizada la creación, el usuario deberá elegir entre las opciones. Así, si se elige una opción pulsando sobre ella, tendremos que capturar y establecer la etiqueta del botón a la elección del usuario, así como destruir la lista desplegable del panel de opciones. Para ello, utilice el siguiente fragmento de código.
else if (sparam == LABEL_OPT1) { // Label "Lots" clicked. Print("LABEL LOTS CLICKED"); // Get the text from LABEL_OPT1 string text = ObjectGetString(0, LABEL_OPT1, OBJPROP_TEXT); // Get the state of the button (enabled or disabled) bool btnState = ObjectGetInteger(0, BTN_LOTS, OBJPROP_STATE); // Set the text of LABEL_LOTS to match LABEL_OPT1 ObjectSetString(0, LABEL_LOTS, OBJPROP_TEXT, text); // Destroy the dropdown list destroyDropDown(); // If the button was previously enabled, disable it if (btnState == true) { ObjectSetInteger(0, BTN_LOTS, OBJPROP_STATE, false); } // Redraw the chart ChartRedraw(0); }
En primer lugar, comprobamos si se ha hecho clic en la primera opción. Si es así, obtenemos el valor del texto de la opción seleccionada y lo establecemos en el valor del texto del botón de volumen de operaciones. Utilizamos una función personalizada «destroyDropDown» para deshacernos del sup-panel creado después de establecer la opción seleccionada por el usuario en el estado del botón, cuyo fragmento de código es el siguiente.
//+------------------------------------------------------------------+ //| Function to destroy dropdown | //+------------------------------------------------------------------+ void destroyDropDown(){ ObjectDelete(0,BTN_DROP_DN); ObjectDelete(0,LABEL_OPT1); ObjectDelete(0,LABEL_OPT2); ObjectDelete(0,LABEL_OPT3); ObjectDelete(0,ICON_DRAG); ChartRedraw(0); }
Por último, comprobamos si el estado del botón estaba previamente habilitado, es decir, en modo pulsado, y si es así, lo deshabilitamos estableciendo la propiedad state a false. La misma lógica se utiliza también en las opciones. A continuación se ofrece un fragmento de código:
else if (sparam==LABEL_OPT2){ Print("LABEL RISK % CLICKED"); string text = ObjectGetString(0,LABEL_OPT2,OBJPROP_TEXT); bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE); ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text); destroyDropDown(); if (btnState==true){ ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false); } ChartRedraw(0); } else if (sparam==LABEL_OPT3){ Print("LABEL MONEY CLICKED"); string text = ObjectGetString(0,LABEL_OPT3,OBJPROP_TEXT); bool btnState = ObjectGetInteger(0,BTN_LOTS,OBJPROP_STATE); ObjectSetString(0,LABEL_LOTS,OBJPROP_TEXT,text); destroyDropDown(); if (btnState==true){ ObjectSetInteger(0,BTN_LOTS,OBJPROP_STATE,false); } ChartRedraw(0); }
Cuando se hace clic en los botones laterales, es decir, los botones de aumento y disminución, tenemos que hacer que respondan aumentando o disminuyendo el valor del campo de edición respectivo. Para empezar, echemos un vistazo al botón de incremento del volumen de operaciones.
else if (sparam == BTN_P1) { // Button "P1" clicked. Increase trading volume. Print(sparam + " CLICKED. INCREASE TRADING VOLUME"); // Get the current trading volume from EDIT_LOTS double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT); // Increment the trading volume by 0.01 trade_lots += 0.01; // Update the value in EDIT_LOTS ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2)); // Redraw the chart ChartRedraw(0); }
Si se pulsa el botón de incremento de volumen de negociación, informamos de la instancia y nos disponemos a incrementar el valor del campo lotes obteniendo su valor actual. Al volumen de negociación obtenido, le añadimos 0,01 como valor de paso incremental. El operador «+=» se utiliza para facilitar el proceso. Lo que hace normalmente es que aumenta el valor del tamaño del lote en 0,01. Es lo mismo que decir (trade_lots = trade_lots + 0,01). El resultado se pasa al campo del lote. El valor doble se convierte en un string y se aplica una precisión de 2 dígitos. La misma lógica prevalece para el botón de disminución, sólo que tenemos que restar 0,01 del valor.
else if (sparam == BTN_M1) { // Button "M1" clicked. Decrease trading volume. Print(sparam + " CLICKED. DECREASE TRADING VOLUME"); // Get the current trading volume from EDIT_LOTS double trade_lots = (double)ObjectGetString(0, EDIT_LOTS, OBJPROP_TEXT); // Decrease the trading volume by 0.01 trade_lots -= 0.01; // Update the value in EDIT_LOTS ObjectSetString(0, EDIT_LOTS, OBJPROP_TEXT, DoubleToString(trade_lots, 2)); // Redraw the chart ChartRedraw(0); }
La misma lógica se aplica a los demás botones similares.
else if (sparam==BTN_P2){ Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS"); double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT); sl_points+=10.0; ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1)); ChartRedraw(0); } else if (sparam==BTN_M2){ Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS"); double sl_points = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT); sl_points-=10.0; ObjectSetString(0,EDIT_SL,OBJPROP_TEXT,DoubleToString(sl_points,1)); ChartRedraw(0); } else if (sparam==BTN_P3){ Print(sparam+" CLICKED. INCREASE STOP LOSS POINTS"); double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT); tp_points+=10.0; ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1)); ChartRedraw(0); } else if (sparam==BTN_M3){ Print(sparam+" CLICKED. DECREASE STOP LOSS POINTS"); double tp_points = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT); tp_points-=10.0; ObjectSetString(0,EDIT_TP,OBJPROP_TEXT,DoubleToString(tp_points,1)); ChartRedraw(0); }
Aquí, especificamos que nuestro paso sea de 10 puntos para los valores de stop loss y take profit. Para cerciorarnos de que vamos por buen camino, recopilamos y visualizamos los resultados a continuación.
Hasta este punto, el progreso es bueno. Los otros botones restantes son los de venta y compra. Su lógica también es bastante simple y sigue la lógica anterior. Para el botón de venta, tenemos la siguiente lógica.
else if (sparam==BTN_SELL){ Print("BTN SELL CLICKED"); ObjectSetInteger(0,BTN_SELL,OBJPROP_STATE,false); double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT); double sell_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT); sell_sl = Ask()+sell_sl*_Point; sell_sl = NormalizeDouble(sell_sl,_Digits); double sell_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT); sell_tp = Ask()-sell_tp*_Point; sell_tp = NormalizeDouble(sell_tp,_Digits); Print("Lots = ",trade_lots,", SL = ",sell_sl,", TP = ",sell_tp); obj_Trade.Sell(trade_lots,_Symbol,Bid(),sell_sl,sell_tp); ChartRedraw(); }
Si el evento de clic es sobre el botón de venta, informamos de la instancia, y establecemos el estado del botón a false, indicando que hemos habilitado la opción de clic. Para abrir una posición de venta, necesitaremos el volumen de negociación, los puntos de stop loss y los puntos de take profit. Obtenemos estos valores y los almacenamos en variables designadas para facilitar su recuperación. Para calcular el stop loss, tomamos los puntos de stop loss y los convertimos al formato de puntos del par de divisas compatible multiplicando por _Point, y añadiendo el valor resultante al precio de venta actual. Posteriormente, normalizamos el valor de salida doble a los dígitos del símbolo para mayor exactitud y precisión. Lo mismo se hace para el nivel de toma de beneficios y, por último, abrimos una posición de venta, introduciendo los lotes comerciales, la cotización de compra como precio de venta, el stop loss y la toma de beneficios. La misma lógica se aplica a una posición de compra, y su lógica es la siguiente.
else if (sparam==BTN_BUY){ Print("BTN BUY CLICKED"); ObjectSetInteger(0,BTN_BUY,OBJPROP_STATE,false); double trade_lots = (double)ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT); double buy_sl = (double)ObjectGetString(0,EDIT_SL,OBJPROP_TEXT); buy_sl = Bid()-buy_sl*_Point; buy_sl = NormalizeDouble(buy_sl,_Digits); double buy_tp = (double)ObjectGetString(0,EDIT_TP,OBJPROP_TEXT); buy_tp = Bid()+buy_tp*_Point; buy_tp = NormalizeDouble(buy_tp,_Digits); Print("Lots = ",trade_lots,", SL = ",buy_sl,", TP = ",buy_tp); obj_Trade.Buy(trade_lots,_Symbol,Ask(),buy_sl,buy_tp); ChartRedraw(); }
Tras las pruebas, he aquí los resultados:
Hasta ahora, todo está funcionando según lo previsto. El usuario podría optar por no utilizar los botones de aumento y disminución y, en su lugar, utilizar directamente las opciones de edición de los campos de los botones de edición. Durante este proceso, podrían producirse errores imprevistos al editar, lo que llevaría a ignorar las operaciones. Por ejemplo, el usuario podría introducir un tamaño de lote de «0.Q7». Técnicamente, este valor no es del todo un número, ya que contiene la letra «Q». Como resultado, no se realizará ninguna operación comercial por debajo del tamaño de lote. Así, asegurémonos de que el valor es válido siempre y, si no es así, indiquemos una instancia del error para que se corrija. Para ello, se utiliza otro ID de evento de gráfico «CHARTEVENT_OBJECT_ENDEDIT».
else if (id==CHARTEVENT_OBJECT_ENDEDIT){ if (sparam==EDIT_LOTS){ Print(sparam+" WAS JUST EDITED. CHECK FOR ANY UNFORESEEN ERRORS"); string user_lots = ObjectGetString(0,EDIT_LOTS,OBJPROP_TEXT); ... } }
En primer lugar, comprobamos si el ID del evento del gráfico es una edición final de un campo de edición. En caso afirmativo, comprobamos si el campo de edición es el botón de volumen de operaciones y, en caso afirmativo, informamos de la instancia y recuperamos el valor introducido por el usuario para un análisis posterior de posibles errores imprevistos. La entrada se almacena en una variable de cadena llamada «user_lots». Para el análisis, necesitaremos dividir el tamaño del lote en partes, donde nuestro límite estará definido por el carácter punto (.).
string lots_Parts_Array[]; int splitCounts = StringSplit(user_lots,'.',lots_Parts_Array);//rep '.' = 'a' Print("User lots split counts = ",splitCounts);ArrayPrint(lots_Parts_Array,0,"<&> ");
Definimos una matriz de almacenamiento dinámico de las partes divididas como una variable de tipo string denominada «lots_Parts_Array». A continuación, dividimos la entrada del usuario con ayuda de la función StringSplit, que toma 3 argumentos. Proporcionamos el valor de la cadena de destino que se va a dividir, entrada de tamaño de lote de usuario en este caso, luego proporcionamos punto como separador y, por último, una matriz de almacenamiento de las subcadenas resultantes. La función devolverá el número de substrings en la matriz de almacenamiento. Si el separador especificado no se encuentra en la string pasada, sólo se colocará un string de origen en la matriz. Estos recuentos de divisiones se almacenarán en la variable de recuento de divisiones. Por último, imprimimos el resultado de los recuentos de división, así como los valores de la matriz, es decir, las substrings resultantes. Si editamos el tamaño de lote a 0,05, esto es lo que obtenemos:
Para que el valor introducido sea válido, debe haber un separador de puntos que dé lugar a dos cuentas divididas. Si es así, significa que la entrada tiene un separador de un solo período.
if (splitCounts == 2){ ... }
En el caso de que el recuento de divisiones sea igual a 1, indica que la entrada carece de punto y, por tanto, no puede aceptarse. En este caso, informamos del error y establecemos una variable booleana llamada «isInputValid» a false.
else if (splitCounts == 1){ Print("ERROR: YOUR INPUT MUST CONTAIN DECIMAL POINTS"); isInputValid = false; }
Si hasta ahora no se cumple ninguna de las dos condiciones, entonces significa que la entrada tiene más de 1 separador de periodos, lo cual es incorrecto, y por lo tanto procedemos a informar del error y a poner el indicador de entrada válida en falso.
else { Print("ERROR: YOU CAN NOT HAVE MORE THAN ONE DECIMAL POINT IN INPUT"); isInputValid = false; }
Si introducimos un valor no válido con separadores de 2 periodos, ésta es la salida que obtenemos en el diario del experto.
Para comprobar si hay caracteres no numerales en la entrada, tendremos que recorrer cada una de las dos divisiones y evaluar cada carácter individualmente. Se necesitará un bucle for para conseguirlo con facilidad.
if (StringLen(lots_Parts_Array[0]) > 0){ // ... }
En primer lugar, nos aseguramos de que la primera cadena, en el índice 0 de la matriz de almacenamiento, no está vacía, caso en el que su longitud de cadena es mayor que 0. Se utiliza una función StringLen para obtener el número de símbolos de la cadena. Si el número de símbolos de la cadena es menor o igual que 0, significa que esa subcadena está vacía, y ese valor de entrada ya es inválido.
else { Print("ERROR: PART 1 (LEFT HAND SIDE) IS EMPTY"); isInputValid = false; }
Para visualizar el error, a continuación se muestra lo que obtenemos si dejamos vacía la parte izquierda del separador.
Para comprobar los caracteres no numerales, utilizamos un bucle for como el que se muestra a continuación.
string split = lots_Parts_Array[0]; for (int i=0; i<StringLen(split); i++){ ushort symbol_code = StringGetCharacter(split,i); string character = StringSubstr(split,i,1); if (!(symbol_code >= 48 && symbol_code <= 57)){ Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code); isInputValid = false; break; } }
Definimos una variable de cadena llamada «split» que es donde almacenamos nuestra primera subcadena en la matriz de almacenamiento. A continuación, iteramos por todos los caracteres de la subcadena. Para el carácter seleccionado, obtenemos el código del carácter utilizando la función StringGetCharacter, una función que devuelve el valor de un símbolo, situado en la posición especificada de una cadena, y almacena el código del símbolo en una variable corta sin signo llamada «symbol_code». Para obtener el carácter del símbolo real, utilizamos la función de subcadena de cadenas. Por último, utilizamos una sentencia if para comprobar si el código resultante se encuentra entre los códigos numéricos y, si no es así, significa que tenemos un carácter no numérico. Así que informamos del error, ponemos el indicador de validez de la entrada en falso y salimos del bucle prematuramente. Si no, significa que los caracteres son todos valores numéricos y nuestra validez de entrada seguirá siendo verdadera.
bool isInputValid = true;
Habrá observado que el intervalo numérico comprendido entre 48 y 57 se considera un intervalo de códigos de símbolos numéricos. Veamos por qué. Según la tabla ASCII, estos símbolos numéricos tienen un sistema de numeración decimal que comienza en 48 para el símbolo «0» y llega hasta 57 para el símbolo «9».
La continuación es la siguiente.
La misma lógica se aplica a la segunda parte de la cadena dividida, es decir, la subcadena situada a la derecha del separador. Su código fuente es el siguiente.
if (StringLen(lots_Parts_Array[1]) > 0){ string split = lots_Parts_Array[1]; for (int i=0; i<StringLen(split); i++){ ushort symbol_code = StringGetCharacter(split,i); string character = StringSubstr(split,i,1); if (!(symbol_code >= 48 && symbol_code <= 57)){ Print("ERROR: @ index ",i+1," (",character,") is NOT a numeral. Code = ",symbol_code); isInputValid = false; break; } } } else { Print("ERROR: PART 2 (RIGHT HAND SIDE) IS EMPTY"); isInputValid = false; }
Para asegurarnos de que podemos diferenciar entre un carácter numérico y un símbolo no numérico, veamos una ilustración.
Puede ver que cuando añadimos una letra mayúscula «A», cuyo código es 65, devolvemos un error, una indicación de que la entrada no es válida. Hemos utilizado «A» en este ejemplo porque su código de símbolo puede consultarse fácilmente en las imágenes proporcionadas. Podría ser cualquier otra cosa. Ahora procedemos de nuevo a utilizar nuestro indicador de validez de entrada para establecer el valor de texto válido en el campo de edición en cuestión.
if (isInputValid == true){ Print("SUCCESS: INPUT IS VALID."); ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,user_lots); ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrBlack); ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrWhite); ChartRedraw(0); }
En caso de que el indicador de validez de la entrada sea igual a verdadero, informamos del éxito y establecemos el valor del texto como la entrada original del usuario, ya que no presenta discrepancias. Volvemos a establecer el color del texto en negro y el color de fondo del botón en blanco. Normalmente, estas son las propiedades originales del campo de edición. Si el resultado es falso, significa que el valor ingresado por el usuario tiene fallas y no se puede usar para operaciones comerciales.
else if (isInputValid == false){ Print("ERROR: INPUT IS INVALID. ENTER A VALID INPUT!"); ObjectSetString(0,EDIT_LOTS,OBJPROP_TEXT,"Error"); ObjectSetInteger(0,EDIT_LOTS,OBJPROP_COLOR,clrWhite); ObjectSetInteger(0,EDIT_LOTS,OBJPROP_BGCOLOR,clrRed); ChartRedraw(0); }
Por lo tanto, informamos del error y establecemos el valor del texto como "Error". Para atraer la máxima atención del usuario, hemos establecido el color del texto en blanco y el color de fondo en rojo, una combinación de colores llamativa que permite al usuario reconocer fácilmente que hay un error. Tras la compilación, obtenemos los siguientes resultados.
Hasta este punto, la automatización de la mayoría de los componentes del panel está completa. Los únicos que quedan sin tener en cuenta son el movimiento de la lista desplegable y el efecto de desplazamiento del ratón sobre un botón. Todo esto debe tenerse en cuenta cuando hay un movimiento del mouse en el gráfico y, por lo tanto, se considerará el ID de evento "CHARTEVENT_MOUSE_MOVE". Para rastrear el movimiento del mouse, necesitaremos habilitar la lógica de detección de movimiento del mouse en el gráfico en la instancia de inicialización experta, y esto se logra a través de la lógica a continuación.
//--- enable CHART_EVENT_MOUSE_MOVE detection ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
Empecemos primero con el más fácil, que es el efecto de desplazamiento. Obtenemos el evento cuando el mouse se mueve en el gráfico después de habilitar su detección.
else if (id==CHARTEVENT_MOUSE_MOVE){ ... }
Para detectar la ubicación del mouse dentro del gráfico, necesitaremos obtener sus ordenadas, es decir su ubicación a lo largo del eje x e y respectivamente, así como su estado, es decir, cuando está en movimiento y cuando está estático.
int mouse_X = (int)lparam; // mouseX >>> mouse coordinates int mouse_Y = (int)dparam; // mouseY >>> mouse coordinates int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving)
Aquí, declaramos una variable de datos de tipo entero "mouse_X" para almacenar la distancia del mouse a lo largo del eje x, o más bien a lo largo de la escala de fecha y hora. Nuevamente, obtenemos el parámetro doble y almacenamos su valor en el parámetro "mouse_Y" y finalmente el parámetro de cadena en la variable "mouse_State". Al final los convertimos en números enteros. Necesitaremos las coordenadas iniciales del elemento de destino, y por eso las definimos a través del fragmento de código a continuación.
//GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE); int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE); int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE); int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE);
Obtenemos las distancias y tamaños respectivos de los botones y los almacenamos en las respectivas variables enteras. El formato de conversión de tipos se utiliza para convertir el valor a formatos enteros. Para realizar un seguimiento de las coordenadas del mouse sobre el botón en cuestión, necesitaremos algunas variables para mantener la lógica.
static bool prevMouseInside = false; bool isMouseInside = false;
La variable booleana estática "prevMouseInside" se declara para realizar un seguimiento de si el mouse estuvo previamente dentro del área del botón. La variable booleana "isMouseInside" almacenará nuestro estado actual del mouse sobre el botón, y todas las variables se inicializan con un indicador falso. Para determinar si el mouse está dentro del área del botón, utilizamos una declaración condicional.
if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn && mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){ isMouseInside = true; }
La verificación condicional determina si el cursor del mouse se encuentra actualmente dentro del área del botón. Si es así, "isMouseInside" se establece como verdadero, lo que indica que el mouse está dentro del cursor; de lo contrario, la variable booleana será falsa si no se cumplen las condiciones. Técnicamente, se deben cumplir cuatro condiciones para que el cursor del mouse se considere dentro del área del botón. Desintegraremos cada condición para una mejor comprensión.
- mouse_X >= XDistance_Hover_Btn: Comprueba si la coordenada X del ratón (mouse_X) es mayor o igual que el límite izquierdo del botón (XDistance_Hover_Btn).
- mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn: Comprueba si la coordenada X del ratón es menor o igual que el límite derecho del botón (suma de XDistance_Hover_Btn y el ancho del botón XSize_Hover_Btn).
- mouse_Y >= YDistance_Hover_Btn: De forma similar, esto comprueba si la coordenada Y del ratón (mouse_Y) es mayor o igual que el límite superior del botón (YDistance_Hover_Btn).
- mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn: Comprueba si la coordenada Y del ratón es menor o igual que el límite inferior del botón (suma de YDistance_Hover_Btn y la altura del botón YSize_Hover_Btn).
Si se cumplen todas las condiciones, establecemos la variable «isMouseInside» en true. Con el valor resultante, podemos comprobar si el ratón está dentro del botón. Se aplica la siguiente lógica.
if (isMouseInside != prevMouseInside) {
Aquí, verificamos si el estado actual del mouse (dentro o fuera del área del botón) ha cambiado desde la última verificación. Asegura que las acciones posteriores solo se realicen cuando haya un cambio en la posición del mouse con respecto al botón. Nuevamente tendremos que comprobar si se cumplieron las condiciones.
// Mouse entered or left the button area if (isMouseInside) { Print("Mouse entered the Button area. Do your updates!"); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue); }
Si la variable boolean es verdadera, significa que el ratón entró en el área del botón. Informamos de la instancia mediante comunicado impreso. Luego, cambiamos el color de la etiqueta del botón así como su fondo. De lo contrario, si la variable es falsa, significa que el cursor del mouse estaba previamente dentro del área del botón y acaba de salir. De esta forma restablecemos los colores a los valores predeterminados. A continuación se muestra el fragmento de código responsable de esa lógica.
else if (!isMouseInside) { Print("Mouse left Btn proximities. Return default properties."); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100'); // Reset button properties when mouse leaves the area ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220'); }
Después de cualquier cambio en las propiedades del botón, se llama a la función «ChartRedraw» para refrescar la visualización del gráfico y reflejar la apariencia actualizada del botón. Por último, la variable «prevMouseInside» se actualiza para que coincida con el estado actual del ratón («isMouseInside»). Esto garantiza que la próxima vez que se active el evento, el programa pueda comparar el nuevo estado con el anterior.
ChartRedraw(0);//// Redraw the chart to reflect the changes prevMouseInside = isMouseInside;
El código completo responsable de crear un efecto al pasar el mouse sobre un botón es el siguiente:
else if (id==CHARTEVENT_MOUSE_MOVE){ int mouse_X = (int)lparam; // mouseX >>> mouse coordinates int mouse_Y = (int)dparam; // mouseY >>> mouse coordinates int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving) //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE); int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE); int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE); int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE); static bool prevMouseInside = false; bool isMouseInside = false; //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn && mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){ isMouseInside = true; } if (isMouseInside != prevMouseInside) { // Mouse entered or left the button area if (isMouseInside) { Print("Mouse entered the Button area. Do your updates!"); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue); } else if (!isMouseInside) { Print("Mouse left Btn proximities. Return default properties."); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100'); // Reset button properties when mouse leaves the area ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220'); } ChartRedraw(0);//// Redraw the chart to reflect the changes prevMouseInside = isMouseInside; } }
Al compilar, esto es lo que obtenemos:
Excelente. Ahora pasamos a la parte final, que no solo consiste en seguir el movimiento del cursor del mouse, sino también en mover objetos o componentes junto con él. Nuevamente, declaramos una variable entera estática para detectar cuándo se hace clic con el mouse y una variable booleana para almacenar el estado de movimiento del cursor del mouse. Esto se logra mediante el fragmento de código a continuación.
// CREATE MOVEMENT static int prevMouseClickState = false; // false = 0, true = 1; static bool movingState = false;
Luego necesitaremos inicializar las variables que contendrán los tamaños y distancias de nuestros objetos.
// INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS // MLB = MOUSE LEFT BUTTON static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press static int mlbDownX_Distance = 0; // Stores the X-distance of an object static int mlbDownY_Distance = 0; // Stores the Y-distance of an object static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN) static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1) static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2) static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3) static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG) static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon
Para inicializar las variables de almacenamiento, declaramos variables de tipo de datos estáticos y las inicializamos a 0 como se indicó anteriormente. Se declaran estáticos ya que necesitamos almacenar los respectivos tamaños y distancias como referencia cuando los componentes del panel están en movimiento. Nuevamente, necesitaremos las distancias de los elementos iniciales y eso se logra mediante el fragmento de código a continuación.
//GET THE INITIAL DISTANCES AND SIZES OF BUTTON int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE); int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE); //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE); //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE); int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE); int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE); int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE); int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE); int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE); int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE); int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE); int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE); int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE); int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE);
Aquí, simplemente usamos la función ObjectGetInteger para recuperar las distancias de los elementos a mover junto con el cursor. Sin embargo, tenga en cuenta que también obtenemos el tamaño del icono que se utilizará en el movimiento del panel. La razón por la que necesitamos los tamaños, tal como lo hicimos en la lógica del efecto de desplazamiento, es que podemos determinar cuándo se hace clic con el cursor del mouse dentro del área del ícono para que podamos comenzar el movimiento. Luego necesitamos capturar la información inicial del clic del mouse y almacenar las distancias de los objetos que se moverán.
if (prevMouseClickState == false && mouse_State == 1) { // Check if the left mouse button was clicked and the mouse is in the pressed state // Initialize variables to store initial distances and sizes of objects mlbDownX = mouse_X; // Store the X-coordinate of the mouse click mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click // Store distances for specific objects mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis) mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis) mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of a specific button (BTN_DROP_DN) mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn; mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of a label (LABEL_OPT1) mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl; mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of another label (LABEL_OPT2) mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl; mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of yet another label (LABEL_OPT3) mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl; // Check if the mouse is within the drag icon area if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon && mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) { movingState = true; // Set the moving state to true } }
Utilizamos una declaración condicional para comprobar dos condiciones. Uno, «prevMouseClickState == false» para asegurar que el botón izquierdo del ratón no ha sido pulsado previamente, y dos, «mouse_State == 1» para comprobar si el ratón está actualmente en estado pulsado (botón presionado). Si se cumplen las dos condiciones, almacenamos las coordenadas X e Y del ratón, así como las distancias de los objetos. Por último, comprobamos si el ratón se encuentra dentro del área del icono de arrastre y, en caso afirmativo, establecemos el estado de movimiento en true, una indicación de que podemos iniciar el movimiento de los componentes del panel. Para entenderlo fácilmente, desglosemos las cuatro condiciones:
- mouse_X >= XDistance_Drag_Icon: Esto verifica que la coordenada X del ratón (mouse_X) es mayor o igual que el límite izquierdo del área del icono de arrastre (XDistance_Drag_Icon).
- mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon: Del mismo modo, garantiza que la coordenada X es menor o igual que el límite derecho del área del icono de arrastre (suma de XDistance_Drag_Icon y la anchura del icono, XSize_Drag_Icon).
- mouse_Y >= YDistance_Drag_Icon: Comprueba si la coordenada Y del ratón (mouse_Y) es mayor o igual que el límite superior del área del icono de arrastre (YDistance_Drag_Icon).
- mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon: Asimismo, verifica que la coordenada Y es menor o igual que el límite inferior del área del icono de arrastre (suma de YDistance_Drag_Icon y la altura del icono, YSize_Drag_Icon).
Si se cumplen las cuatro condiciones (es decir, el ratón está dentro del área definida del icono de arrastre), establecemos la variable «movingState» en true. En este punto, si el estado de movimiento es verdadero, movemos los objetos designados.
if (movingState){ ChartSetInteger(0,CHART_MOUSE_SCROLL,false); ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX); ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY); ... ChartRedraw(0); }
Aquí utilizamos la función ChartSetInteger para desactivar el indicador de desplazamiento del gráfico. Esto garantizará que cuando el mouse se mueva, el gráfico no se desplazará horizontalmente. De esta forma, únicamente el cursor del ratón se moverá junto con los objetos designados. Por último, establecemos las nuevas distancias de los objetos, respecto a las coordenadas actuales del mouse, y redibujamos el gráfico para que los cambios surtan efecto. En pocas palabras, esto es lo que tenemos:
Ahora puedes ver que podemos arrastrar el icono. Sin embargo, también necesitamos arrastrarlo junto con los otros componentes del panel. Por lo tanto, se aplica la misma lógica.
ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX); ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY);
La adición de otros elementos de lógica de arrastre garantizará que mientras el ícono de arrastre se mueve, los otros componentes del panel también se muevan. Al compilar esto es lo que obtenemos:
Ha sido un éxito. Puedes ver que todos los componentes del panel se mueven junto con el cursor del mouse. Sin embargo, hay un pequeño problema que debemos solucionar. Cuando se suelta el ratón, es decir, no está en modo presionado, los componentes continúan moviéndose a medida que se mueve el cursor. Para liberar el panel del estado de movimiento, debemos establecer el estado en falso en el caso de que no se presione el mouse.
if (mouse_State == 0){ movingState = false; ChartSetInteger(0,CHART_MOUSE_SCROLL,true); }
Si el estado del mouse es igual a cero, significa que el botón izquierdo del mouse está liberado y, por lo tanto, establecemos el estado de movimiento en falso, lo que indica que no necesitamos mover más los componentes del panel. Posteriormente, habilitamos el evento de desplazamiento del gráfico estableciendo el indicador en verdadero. Finalmente, establecemos el estado anterior del mouse en el estado actual del mouse.
prevMouseClickState = mouse_State;
El código fuente final responsable de la automatización del efecto de desplazamiento y el movimiento del panel es el siguiente:
else if (id==CHARTEVENT_MOUSE_MOVE){ int mouse_X = (int)lparam; // mouseX >>> mouse coordinates int mouse_Y = (int)dparam; // mouseY >>> mouse coordinates int mouse_State = (int)sparam; // Get the mouse state (0 = mouse moving) //GETTING THE INITIAL DISTANCES AND SIZES OF BUTTON int XDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XDISTANCE); int YDistance_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YDISTANCE); int XSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_XSIZE); int YSize_Hover_Btn = (int)ObjectGetInteger(0,BTN_HOVER,OBJPROP_YSIZE); static bool prevMouseInside = false; bool isMouseInside = false; //Print("Mouse STATE = ",mouse_State); // 0 = mouse moving if (mouse_X >= XDistance_Hover_Btn && mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn && mouse_Y >= YDistance_Hover_Btn && mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn){ isMouseInside = true; } if (isMouseInside != prevMouseInside) { // Mouse entered or left the button area if (isMouseInside) { Print("Mouse entered the Button area. Do your updates!"); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'050,050,255'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, clrLightBlue); } else if (!isMouseInside) { Print("Mouse left Btn proximities. Return default properties."); //createRecLabel(BTN_HOVER,25,230,220,35,C'220,220,220',3,C'100,100,100'); // Reset button properties when mouse leaves the area ObjectSetInteger(0, BTN_HOVER, OBJPROP_COLOR, C'100,100,100'); ObjectSetInteger(0, BTN_HOVER, OBJPROP_BGCOLOR, C'220,220,220'); } ChartRedraw(0);//// Redraw the chart to reflect the changes prevMouseInside = isMouseInside; } // CREATE MOVEMENT static int prevMouseClickState = false; // false = 0, true = 1; static bool movingState = false; // INITIALIZE VARIBALES TO STORE INITIAL SIZES AND DISTANCES OF OBJECTS // MLB = MOUSE LEFT BUTTON static int mlbDownX = 0; // Stores the X-coordinate of the mouse left button press static int mlbDownY = 0; // Stores the Y-coordinate of the mouse left button press static int mlbDownX_Distance = 0; // Stores the X-distance of an object static int mlbDownY_Distance = 0; // Stores the Y-distance of an object static int mlbDownX_Distance_BTN_DROP_DN = 0; // Stores X-distance for a specific button (BTN_DROP_DN) static int mlbDownY_Distance_BTN_DROP_DN = 0; // Stores Y-distance for the same button static int mlbDownX_Distance_LABEL_OPT1 = 0; // Stores X-distance for a label (LABEL_OPT1) static int mlbDownY_Distance_LABEL_OPT1 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_LABEL_OPT2 = 0; // Stores X-distance for another label (LABEL_OPT2) static int mlbDownY_Distance_LABEL_OPT2 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_LABEL_OPT3 = 0; // Stores X-distance for yet another label (LABEL_OPT3) static int mlbDownY_Distance_LABEL_OPT3 = 0; // Stores Y-distance for the same label static int mlbDownX_Distance_ICON_DRAG = 0; // Stores X-distance for an icon (ICON_DRAG) static int mlbDownY_Distance_ICON_DRAG = 0; // Stores Y-distance for the same icon //GET THE INITIAL DISTANCES AND SIZES OF BUTTON int XDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE); int YDistance_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE); //int XSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_XSIZE); //int YSize_DropDn_Btn = (int)ObjectGetInteger(0,BTN_DROP_DN,OBJPROP_YSIZE); int XDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE); int YDistance_Opt1_Lbl = (int)ObjectGetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE); int XDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE); int YDistance_Opt2_Lbl = (int)ObjectGetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE); int XDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE); int YDistance_Opt3_Lbl = (int)ObjectGetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE); int XDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE); int YDistance_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE); int XSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_XSIZE); int YSize_Drag_Icon = (int)ObjectGetInteger(0,ICON_DRAG,OBJPROP_YSIZE); if (prevMouseClickState == false && mouse_State == 1) { // Check if the left mouse button was clicked and the mouse is in the pressed state // Initialize variables to store initial distances and sizes of objects mlbDownX = mouse_X; // Store the X-coordinate of the mouse click mlbDownY = mouse_Y; // Store the Y-coordinate of the mouse click // Store distances for specific objects mlbDownX_Distance = XDistance_Drag_Icon; // Distance of the drag icon (X-axis) mlbDownY_Distance = YDistance_Drag_Icon; // Distance of the drag icon (Y-axis) mlbDownX_Distance_BTN_DROP_DN = XDistance_DropDn_Btn; // Distance of BTN_DROP_DN mlbDownY_Distance_BTN_DROP_DN = YDistance_DropDn_Btn; mlbDownX_Distance_LABEL_OPT1 = XDistance_Opt1_Lbl; // Distance of LABEL_OPT1 mlbDownY_Distance_LABEL_OPT1 = YDistance_Opt1_Lbl; mlbDownX_Distance_LABEL_OPT2 = XDistance_Opt2_Lbl; // Distance of LABEL_OPT2 mlbDownY_Distance_LABEL_OPT2 = YDistance_Opt2_Lbl; mlbDownX_Distance_LABEL_OPT3 = XDistance_Opt3_Lbl; // Distance of LABEL_OPT3 mlbDownY_Distance_LABEL_OPT3 = YDistance_Opt3_Lbl; // Check if the mouse is within the drag icon area if (mouse_X >= XDistance_Drag_Icon && mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon && mouse_Y >= YDistance_Drag_Icon && mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon) { movingState = true; // Set the moving state to true } } if (movingState){ ChartSetInteger(0,CHART_MOUSE_SCROLL,false); ObjectSetInteger(0,ICON_DRAG,OBJPROP_XDISTANCE,mlbDownX_Distance + mouse_X - mlbDownX); ObjectSetInteger(0,ICON_DRAG,OBJPROP_YDISTANCE,mlbDownY_Distance + mouse_Y - mlbDownY); ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_XDISTANCE,mlbDownX_Distance_BTN_DROP_DN + mouse_X - mlbDownX); ObjectSetInteger(0,BTN_DROP_DN,OBJPROP_YDISTANCE,mlbDownY_Distance_BTN_DROP_DN + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT1,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT1 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT1,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT1 + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT2,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT2 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT2,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT2 + mouse_Y - mlbDownY); ObjectSetInteger(0,LABEL_OPT3,OBJPROP_XDISTANCE,mlbDownX_Distance_LABEL_OPT3 + mouse_X - mlbDownX); ObjectSetInteger(0,LABEL_OPT3,OBJPROP_YDISTANCE,mlbDownY_Distance_LABEL_OPT3 + mouse_Y - mlbDownY); ChartRedraw(0); } if (mouse_State == 0){ movingState = false; ChartSetInteger(0,CHART_MOUSE_SCROLL,true); } prevMouseClickState = mouse_State; }
En pocas palabras, esto es lo que hemos logrado.
Esto fue genial. Acabamos de darle vida a nuestro panel GUI y ahora nuestro panel es interactivo y responsivo. Tiene efectos de desplazamiento, clics de botones, actualizaciones de datos en vivo y responde a los movimientos del mouse.
Conclusión
En conclusión, a partir de la implementación del artículo, podemos decir que integrar características dinámicas en un panel GUI MetaQuotes Language 5 (MQL5) mejora significativamente la experiencia del usuario haciéndola más interactiva y funcional. Si se añaden efectos hover a los botones, se crea una interfaz visualmente atractiva que responde intuitivamente a las acciones del usuario. La actualización en tiempo real de los precios de compra y venta garantiza que los operadores dispongan de la información más actualizada del mercado, lo que les permite tomar decisiones informadas con rapidez. Los botones clicables para ejecutar órdenes de compra y venta, así como la función de cierre de posiciones y órdenes, agilizan las operaciones de negociación, permitiendo a los usuarios reaccionar con prontitud a los cambios del mercado.
Además, la implementación de subpaneles móviles y listas desplegables añade una capa de personalización y flexibilidad a la interfaz. Los comerciantes pueden organizar su espacio de trabajo según sus preferencias, mejorando su eficiencia general. La función de listas desplegables permite acceder cómodamente a varias opciones sin saturar la interfaz principal, lo que contribuye a crear un entorno de negociación más limpio y organizado. En general, estas mejoras transforman el panel MQL5 GUI en una herramienta robusta y fácil de usar que satisface las necesidades de los operadores modernos, mejorando en última instancia su experiencia y eficacia en el trading. Los operadores pueden utilizar los conocimientos ilustrados para crear paneles GUI más complejos y atractivos que mejoren su experiencia de negociación. Esperamos que el artículo le haya parecido detallado, objetivamente explicado y fácil de seguir y aprender. ¡Salud!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15263
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- 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
Excelente trabajo, ¡gracias!
@Clemence Benjamin gracias por el comentario y el reconocimiento. Es muy amable de tu parte. Bienvenido.
Impresionante trabajo, gracias por compartirlo.
Gracias, muy buen material, se puede hacer un cierre parcial en una cuenta de compensación ?
Porque la orden contraria se abre....