Разработка интерактивного графического пользовательского интерфейса на MQL5 (Часть 2): Добавление элементов управления и адаптивности
Введение
В предыдущей статье мы заложили основу, собрав графические элементы панели GUI на языке MetaQuotes Language 5 (MQL5). Если помните, итерация представляет собой статическую сборку элементов графического интерфейса - просто застывший во времени снимок, лишенный возможности реагирования. Он был статичным и непроизводительным. Теперь разморозим этот снимок и вдохнем в него жизнь. В долгожданном продолжении выведем нашу панель на следующий уровень. Пристегнитесь, и мы узнаем, как вдохнуть жизнь и в интерфейс:
- Макет и адаптивность: Забудьте о статичных компонентах! Будем использовать относительное позиционирование, гибкие макеты и кликабельные, адаптивные и редактируемые компоненты, чтобы наша панель отзывалась на действия пользователей.
- Динамические обновления: данные в реальном времени — основа любого торгового приложения. Займемся получением актуальных ценовых потоков в реальном времени и сделаем так, чтобы наша панель отражала самую свежую рыночную информацию.
- Мобильность компонентов: Представьте себе перетаскиваемые элементы - панели, реагирующие на прикосновение пользователя. Рассмотрим, как сделать определенные компоненты подвижными, улучшая качество трейдинга для пользователей.
Следующие темы помогут нам создать адаптивную и интерактивную панель:
- Иллюстрация автоматизируемых элементов
- Автоматизация графического интерфейса на MQL5
- Заключение
Иллюстрация автоматизируемых элементов
Необходимо автоматизировать семь компонентов. Первый компонент _ закрытие панели при нажатии соответствующей кнопки. Мы намерены удалить все элементы панели при нажатии этой кнопки. В-вторых, при нажатии кнопок управления позициями соответствующие позиции и ордера будут закрыты в соответствии с инструкциями. Например, когда мы нажимаем на кнопку или метку «Прибыль», мы закрываем только все те позиции, которые находятся в прибыли. Третий автоматизируемый компонент — объем торговли. После нажатия на собственные средства будет создан раскрывающийся список опций, позволяющий пользователю выбрать способ торговли.
Четвертую автоматизацию реализуем на кнопках увеличения или уменьшения рядом с соответствующими кнопками торговли для увеличения или уменьшения значений в полях редактирования вместо того, чтобы просто вводить их. Если пользователь захочет ввести нужные значения напрямую, поле редактирования должно будет фиксировать введенные значения, и это будет наш пятый шаг автоматизации. Шестым будет создание эффекта наведения на выбранную кнопку. То есть, когда курсор мыши находится в области кнопки наведения, кнопка увеличится, указывая, что курсор приблизился к ней, а отодвинутый от этой области курсор сбросит состояние кнопки до значений по умолчанию. Наконец, будем обновлять котировки до текущих значений в реальном времени на каждом ценовом тике.
Для облегчения этих процессов автоматизации и компонентов ниже приводится подробное опписание каждого из них с учетом предыдущего этапа.

Получив представление о намечающихся действиях, давайте сразу приступим к автоматизации. Если вы еще этого не сделали, ознакомьтесь с предыдущей статьей, пожалуйста. Там мы создали статическую сборку элементов графического интерфейса, так что вы от нас не отстанете. Давайте сделаем это.
Автоматизация графического интерфейса на MQL5
Перейдем от простых процессов к сложным, чтобы организовать нашу структуру в хронологическом порядке. Таким образом, будем обновлять цены на каждом тике или при каждом изменении котировки. Чтобы добиться этого, нам понадобиться обработчик событий OnTick, встроенная функция MQL5, которая обычно вызывается при изменении котировок. Функция имеет тип данных void, и это означает, что она обрабатывает исполнения напрямую и ей не нужно возвращать какие-либо выходные данные. Ваша функция должна выглядеть примерно так, как показано ниже.
//+------------------------------------------------------------------+ //| 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, Bid());
Используем ObjectSetStringдля установки свойства объекта, в данном случае текста, поскольку нам нужно изменить входные текстовые данные метки кнопки. Представим идентификатор окна как 0 для текущего графика, или можно использовать ChartID() — функцию, которая представит индекс идентификации графика для текущего окна. Затем укажем LABEL_SELL_PRICE в качестве имени целевого объекта для обновления метки кнопки продажи и OBJPROP_TEXT — для обозначения, что обновляемое нами свойство объекта — текстовое строковое значение объекта. Наконец, укажем текстовое значение. Цена bid — значение, которое нам нужно обновить, поэтому мы его заполняем. Но это еще не всё. Тип свойства, который нам нужно заполнить, представляет собой значение строковых данных, а наша цена bid указана в формате double. Таким образом, нам нужно преобразовать значение типа double в значение типа string, иначе при компиляции получим предупреждение — неявное преобразование из number в string.

На этом этапе можно было бы напрямую преобразовать значение double в string, как показано ниже, но обычно это не рекомендуется, ведь здесь нужен обдуманный подход.
// --- 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());
Приведение типов в MQL5, как и преобразование числового значения в строку, имеет некоторые нюансы. Один из наиболее распространенных — потеря точности. Например, наша цена bid представляет собой значение с плавающей запятой, а если, к примеру, цена равна 11.77900, двумя последними нулями можно пренебречь, а окончательное выходное значение будет 11.779. Технически между этими двумя значениями нет логической разницы, но визуально и математически есть, так как в одном из них 5 цифр, а в другом — 3. Вот пример того, что мы имеем в виду.

Как мы видели, приведение типов позволит избавиться от предупреждения, но это не лучший подход, когда важна точность. Так что понадобится ещё одна функция. Для выполнения преобразования воспользуемся встроенной функцией MQL5: DoubleToString. Эту функцию используют для преобразования числового значения с плавающей запятой в текстовую строку. Она принимает два входных параметра или аргумента: целевое значение с плавающей запятой и формат точности. В нашем случае используем в качестве целевого значения цену bid, а для формата точности — _Digits, переменную, хранящую количество знаков после запятой, определяющее точность цены символа текущего графика. Вы тоже можете использовать функцию Digits(). Это может быть произвольное число в диапазоне от 0 до 8, и если его не указать, то его значение будет иметь 8 цифр. Например, наш символ — GOLD (XAUUSD) с 3 цифрами. Таким образом, наше числовое значение будет равно 3, но для автоматизации и придания коду адаптируемости к валютным парам воспользуемся функцией, чтобы автоматически получать количество цифр конкретной валютной пары. Однако, если вам нужен фиксированный диапазон количества знаков после запятой, используйте статическое значение. Вот окончательный код для этой настройки цены bid.
// --- 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));
Поскольку теперь у нас правильная логика преобразования (спасибо разработчикам MQL5 за прекрасную функцию), ниже будут результаты.

Для установки цены спроса кнопки покупки и спреда применяется та же логика. Вот код для этого.
// --- 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());
Вы должны были заметить, что для спреда мы напрямую выполняем приведение типа его строкового значения, хотя ранее мы критиковали этот подход с точки зрения сохранения точности. Здесь функция спреда представляет собой данные целочисленного типа, поэтому точность не является основным приоритетом, ведь формат у нас будет корректным в любом случае. Однако для преобразования вы могли бы использовать и функцию IntegerToString, что в результате даст то же значение.
// Set the text of the "SPREAD" button to the current spread value ObjectSetString(0, BTN_SPREAD, OBJPROP_TEXT, IntegerToString(Spread()));
Функция принимает три аргумента, но достаточно только целевого значения, поскольку формат точности не указан. Теперь можно почувствовать разницу. Вот чего мы достигли на данном этапе в формате графических данных (GIF).

Это все, что нам нужно сделать в обработчике событий, а полный исходный код, ответственный за обновление цен, выглядит следующим образом:
//+------------------------------------------------------------------+ //| 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())); } //+------------------------------------------------------------------+
Итак, первый компонент автоматизации готов. Было нетрудно, так ведь? Тогда перейдем к другим компонентам нашей панели графического интерфейса. Оставшиеся элементы будут автоматизированы внутри обработчика функции OnChartEvent, поэтому рассмотрим подробнее его входные параметры и функции.
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { ... }
Цель функции — обработка изменений на графике, внесенных пользователем или MQL5-программой. Таким образом, выполняемые пользователем действия, такие как перемещения мыши, редактирование полей кнопок и нажатие на метках и кнопках, будут зафиксированы и обработаны этим обработчиком событий. Разберем его аргументы для более глубокой интерпретации:
- id: Этот параметр — идентификатор событий, соответствующий одному из предопределенных 11 типов событий. К ним относятся такие события, как нажатия клавиш, движения мышью, создание объектов, изменения графика и пользовательские события. Для пользовательских событий можно использовать идентификаторы от CHARTEVENT_CUSTOM до CHARTEVENT_CUSTOM_LAST. 11 типов событий показаны ниже;

- lparam: Параметр события типа long. Его значение зависит от конкретного обрабатываемого события. Например, он может представлять код клавиши во время события нажатия клавиши.
- dparam: Параметр события типа double. Как и у параметра lparam, его значение меняется в зависимости от типа события. Например, во время события перемещения мыши он может передавать положение курсора.
- sparam: Параметр события типа string. Опять же, его значение зависит от события. Например, при создании объекта он может содержать имя вновь созданного объекта.
Чтобы показать это более наглядно, сделаем внутри функции печать в журнал, содержащую все четыре аргумента.
// Print the 4 function parameters Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam);
Эта функция напечатает идентификатор события графика, его значение события long, значение события типа double и значение события типа string. Для удобства отсылки рассмотрим следующий GIF.

Из представленного GIF-изображения теперь все должно быть понятно. Теперь перейдем к регистрации событий — щелчков на элементах панели графического интерфейса. Таким образом, нашим идентификатором будет CHARTEVENT_OBJECT_CLICK.
//Print("ID = ",id,", LPARAM = ",lparam,", DPARAM = ",dparam,", SPARAM = ",sparam); if (id==CHARTEVENT_OBJECT_CLICK){ ... }
Сначала закомментируем предыдущую строку кода, поскольку не хотим засорять наш журнал ненужной информацией. Две косые черты (//) называются однострочными комментариями, которые используются для кода от того места, где они поставлены, и до конца строки, отсюда и их название. Во время исполнения кода компьютер в принципе игнорирует комментарии. Используем оператор if для проверки, были ли щелчок на объекте. Этого можно добиться, приравняв идентификатор события графика к перечислениям щелчков на объекте. Если мы щелкнули на объекте, выведем аргументы и посмотрим, что получилось. Используется следующий код.
if (id==CHARTEVENT_OBJECT_CLICK){ Print("ID = ",id,", LM = ",lparam,", DM = ",dparam,", SPARAM = ",sparam); ... }
В функции вывода на печать мы просто заменили LPARAM на LP, а DPARAM — на DP, чтобы можно было сосредоточиться на идентификаторе события графика и имени выбранного объекта. Оттуда получим идентификатор объекта и при необходимости совершим действие. Ниже приводится иллюстрация этой логики:

Первой функцией автоматизации компонентов станет уничтожение панели графического интерфейса при нажатии на иконку «скорая помощь». На приведенном выше GIF видно, что после щелчка на объекте его имя сохраняется в переменной типа строкового события. Таким образом, из этой переменной мы можем получить имя выбранного объекта и проверить, является ли он нужным нам объектом. Если да, можем совершить действие, в нашем случае — уничтожить панель.
//--- if icon car is clicked, destroy the panel if (sparam==ICON_CAR){ Print("BTN CAR CLICKED. DESTROY PANEL NOW"); destroyPanel(); ChartRedraw(0); }
Другой оператор if используется для проверки экземпляра, в котором была кликнута иконка автомобиля, и если это так, то мы информируем экземпляр о щелчке на нем, и можно выполнить уничтожение панели, поскольку это правильная иконка для этого действия. Затем вызовем функцию destroyPanel function, цель которой — удалить каждый элемент нашей панели. Эта функция уже должна быть вам знакома, ведь мы использовали ее в предыдущей статье (Часть 1). Наконец, вызываем функцию ChartRedraw. Функцию используют для принудительной перерисовки указанного графика. При программном изменении свойств или объектов графика (например, индикаторов, линий или фигур) изменения могут не сразу отобразиться на графике. Вызывая эту функцию, вы обеспечиваете актуализацию графика и отображение на нем последних изменений. Вот наглядное представление полученных результатов.

Как видите, логика довольно проста. Тот же метод следует использовать для щелчков на других объектах. Теперь перейдем к события щелчка на метке кнопки закрытия. Когда это происходит, нужно закрыть все открытые позиции и удалить все отложенные ордера. Это обеспечит гарантированное отсутствие рыночных ордеров. Оператор else-if потребуется для проверки условия нажатия кнопки закрытия.
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); ... }
Здесь мы хотим несколько изменить экземпляр события. Нам хотим иметь возможность при нажатии кнопки менять ее цвет, указывая на факт нажатия кнопки, чтобы инициировать процесс закрытия рыночных ордеров. После завершения закрытия нам понадобится сбросить цвет кнопки до значения по умолчанию. Для получения исходного цвета метки кнопки объявим переменную данных типа long с именем originalColor и сохраним в ней цвет кнопки по умолчанию. Для получения цвета кнопки воспользуемся функцией ObjectGetInteger, передадим идентификатор графика, название кнопки и свойство кнопки — цвет в нашем случае. После сохранения исходного цвета мы теперь можем изменять цвет надписи на кнопке, поскольку у нас уже есть резерв ее исходного значения. Воспользуемся функцией ObjectSetInteger для установки красного цвета объекта. Находясь в этом состоянии, мы инициируем процесс закрытия ордеров.
// 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 } } } }
Мы используем for loop для перебора всех открытых позиций и закрытия их. Для получения всех открытых позиций воспользуемся встроенной функцией MQL5 — PositionsTotal. Эта функция вернет количество открытых позиций на данном торговом счете. Затем получаем тикет этой позиции, предоставляя ее индекс в функцию PositionGetTicket, и сохраняем его в переменной данных типа ulong с именем ticket. Функция возвращает тикет указанной позиции, а в случае неудачи возвращает 0. Чтобы продолжить, нам нужно убедиться, что у нас есть тикет. Это достигается путём использования оператора if, чтобы убедиться, что значение тикета больше 0. Если это так, это означает, что у нас есть тикет и мы продолжаем выбирать тикет, чтобы иметь возможность работать с ним. При успешном выборе тикета мы можем получить информацию о позиции. Поскольку на этом отдельном торговом счете может быть несколько позиций, убедимся, что мы закрываем только позиции, связанные с конкретной валютной парой. Наконец, мы закрываем эту позицию по номеру тикета и продолжаем выполнять то же самое для других открытых позиций, если таковые имеются.
Однако для закрытия позиции мы используем объект obj_Trade, за которым следует оператор dot. Это называется объектом класса. Для удобства выполнения операции закрытия позиций нам нужно включить экземпляр класса, который поможет этому процессу. Таким образом, в начале исходного кода мы включим торговый экземпляр с помощью #include. Это дает нам доступ к классу CTrade, который мы используем для создания торгового объекта. Это чрезвычайно важно, поскольку он нужен нам для выполнения торговых операций.
#include <Trade/Trade.mqh>
CTrade obj_Trade; Препроцессор заенит строку #include <Trade/Trade.mqh> содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартной директории (обычно это terminal_installation_directory\MQL5\Include). Текущая директория в поиск не включается. Строку можно разместить в любом месте программы, но обычно все включения размещаются в начале исходного кода, чтобы улучшить его структуру и обеспечить удобство ссылок. Объявление объекта obj_Trade класса CTrade с легкостью даст нам доступ к содержащимся в этом классе методам — спасибо разработчикам MQL5.

Для удаления отложенных ордеров используется та же логика итерации.
// 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 } } } }
Главное отличие в логике итерации заключается в том, что мы воспользуемся функцией OrdersTotal для получения общего количества ордеров. Все остальное привязано к ордерам. После закрытия всех позиций и удаления ордеров нам понадобится сброс цвета метки на кнопке к исходному цвету.
// 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);
Функция ObjectSetInteger используется путем передачи идентификатора графика, названия кнопки, свойства цвета и исходного цвета. Вот здесь-то наша предыдущая переменная оказывается очень кстати. Нам не нужно постоянно стараться запомнить исходный цвет объекта, поскольку можем сохранять и находить его автоматически. Полный код, обеспечивающий закрытие всех открытых позиций и удаление всех открытых ордеров, выглядит следующим образом:
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); }
После каждого добавления логики на панель всегда рекомендуется компилировать и запускать код, чтобы, прежде чем перейти к другой логике управления, убедиться, что все работает так, как и предполагалось. Вот чего нам удалось добиться на данный момент.

Теперь мы можем успешно закрыть позиции и ордера. Обратите внимание, что при нажатии кнопки закрытия во время закрытия позиций цвет метки кнопки остается красным, пока все позиции не будут закрыты, а в конечном итоге возвращается к исходному цвету. Кроме того, вы можете заметить, что мы не закрываем позицию на покупку AUDUSD, потому что советник (EA) в настоящее время прикреплен к символу Gold. Теперь ту же логику можно использовать для настройки адаптивности других меток на кнопках.
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); }
Отличие этого кода от кода кнопки закрытия состоит в том, что мы избавляемся от итерации закрытия ордера, поскольку хотим закрыть только все открытые позиции. Для закрытия всех прибыльных позиций используется приведенный ниже фрагмент кода.
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); }
Основное отличие этого фрагмента кода от предыдущего, который должен закрывать все открытые позиции, заключается в том, что мы добавляем дополнительную логику, чтобы проверить, превышает ли прибыль данной позиции нулевое значение, то есть закрываем лишь те позиции, которые находятся в прибыли. Ниже приведена конкретная логика:
double profit_or_loss = PositionGetDouble(POSITION_PROFIT); if (profit_or_loss > 0) { obj_Trade.PositionClose(ticket); // Close the position }
Определим переменную типа данных double с названием profit_or_loss и сохраним в ней текущую плавающую прибыль или плавающий убыток по выбранной позиции. Если значение превышает 0, закрываем позицию, поскольку она уже прибыльная. Та же логика применяется к кнопку убытка, как показано ниже: закрываем позицию только в том случае, если она убыточная.
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); }
Наконец, для закрытия отложенных ордеров при нажатии на соответствующую кнопку используется итерация ордеров, кода которой выглядит следующим образом.
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); }
Ниже представлена визуализация основных этапов.

Как показано на рисунке, что кнопки заголовков нашей панели теперь реагируют на нажатие. Перейдем к оживлению кнопки объема торговли. Мы хотим, чтобы при нажатии на кнопку или на самой метке либо при нажатии на иконку раскрывающегося меню создавалась другая подпанель с различными списками опций, из которых пользователь может выбирать нужные. Логика показана ниже:
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); }
После нажатия кнопки информируем об экземпляре и устанавливаем состояние кнопки на true. В результате кнопка становится темнее, что означает: на нее нажали. После этого создаем выпадающий список, вызывая пользовательскую функцию createDropDown, фрагмент кода которой был ранее представлен в первой статье. После создания этого списка пользователю нужно будет выбирать среди опций. Таким образом, если опция выбрана нажатием на нее, нам придется зафиксировать и установить метку кнопки в соответствием с выбором пользователя, а также уничтожить выпадающий список панели с опциями. Достигнем этого с помощью приведенного ниже фрагмента кода.
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); }
Сначала проверим, была ли выбрана первая опция. Если да, получаем текстовое значение выбранной опции и устанавливаем его текстовым значением кнопки объема торговли. Воспользуемся пользовательской функцией destroyDropDown, чтобы избавиться от созданной подпанели после установки выбранной пользователем опции в состояние кнопки. Фрагмент кода приведен ниже.
//+------------------------------------------------------------------+ //| 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); }
Наконец, проверяем, было ли состояние кнопки ранее включено, то есть находится ли кнопка в нажатом состоянии. Если да, отключим его, установив свойство состояния в значение false. В опциях тоже используется эта логика. Ниже приводится фрагмент их кода:
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); }
При нажатии боковых кнопок, то есть кнопок увеличения и уменьшения нам нужно сделать их способными быстро реагировать на увеличение или уменьшение значения соответствующего поля редактирования. Для начала рассмотрим кнопку увеличения объема торговли.
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); }
При нажатии на кнопку увеличения объема торговли информируем об этом экземпляре и готовимся увеличить значение поля лотов, получив его текущее значение. К полученному объему торговли добавим 0.01 в качестве значения приращения шага. Для упрощения процесса используется оператор +=. Обычно он увеличивает значение размера лота на 0.01. Это то же самое, что сказать: trade_lots = trade_lots + 0.01. Затем результат передается в поле лота. Значение double преобразуется в строковое, и применяется точность до 2 знаков. Та же логика преобладает для кнопки уменьшения, только из значения нам нужно вычесть 0.01.
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); }
Та же логика применима и к другим подобным кнопкам.
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); }
Здесь мы указываем шаг равным 10 пунктам для значений Stop Loss и Take Profit. Чтобы убедиться, что мы на правильном пути, соберем и визуализируем результаты ниже.

На данный момент все идет хорошо. Прочие оставшиеся кнопки — sell и buy. Их логика тоже достаточно проста и следует предшествующей логике. Для кнопки sell у нас есть следующая логика.
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(); }
Если событие щелчка происходит на кнопке sell, информируем об экземпляре и устанавливаем состояние кнопки на false, указывая, что мы активировали опцию щелчка. Для открытия позиции на продажу нам понадобятся объем сделки и точки стоп-лосса и тейк-профита. Получим эти значения и сохраним их в назначенных переменных, чтобы их было проще извлечь. Для расчета стоп-лосса возьмем его точки и преобразуем их в формат точек совместимой валютной пары, умножив на _Point и прибавив полученное значение к текущей цене ask. Позже, в целх точности и достоверности, мы нормализуем выходное значение double до цифр символа. То же самое проделываем для уровня тейк-профита и, наконец, открываем позицию на продажу, передавая торговые лоты, котировку bid в качестве цены продажи, стоп-лосс и тейк-профит. Та же логика применима и к позиции на покупку и выглядит следующим образом.
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(); }
Вот результаты тестирования:

На данный момент все работает, как и ожидалось. Пользователь мог бы не использовать кнопки увеличения и уменьшения, а воспользоваться вместо этого непосредственно опциями редактирования в полях кнопки редактирования. В ходе этого процесса при редактировании могут возникать непредвиденные ошибки, которые могут привести к игнорированию операций. Например, пользователь мог ввести размера лота как 0.Q7. Технически это значение не вполне численное, поскольку содержит букву Q. В результате торговые операции под этим размером лота проводиться не будут. Таким образом, убедимся, что значение всегда является допустимым, а если это не так, укажем пример ошибки, которую нужно исправить. Для достижения этого используется другой идентификатор события графика — 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); ... } }
Сначала проверим, является ли идентификатор события графика окончанием редактирования соответствующего поля. Если это так, проверим, является ли поле редактирования кнопкой объема сделки. Если да, информируем об экземпляре и извлекаем пользовательское входное значение для дальнейшего анализа потенциальных непредвиденных ошибок. Входные данные сохраняются в строковой переменной с именем user_lots. В целях анализа нам потребуется разбить размер лотта на части, где нашу границу будет обозначать символ точки (.), который в английском языке часто называют по-разному: full stop, point и dot.
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,"<&> ");
Определим динамический массив хранения разделенных частей как переменную данных типа string с именем lots_Parts_Array. Затем разделим пользовательские входные данные с помощью функции StringSplit, которая принимает 3 аргумента. Представляем целевое строковое значение, которое необходимо разделить на части, в данном случае — пользовательские входные данные о размере лота, затем в качестве разделителя указываем точку и, наконец, массив хранения полученных подстрок. Функция вернет количество подстрок в массиве хранения. Если в переданной строке указанный разделитель не найден, в массив будет помещена только одна исходная строка. Эти значения количества разделений будут сохраняться в соответствующей переменной (split count). Наконец, напечатаем результат подсчета разделений, а также значения массива, то есть полученные подстроки. Если мы изменим размер лота на 0.05, то получим следующее:

Чтобы входное значение было валидным, в качестве разделителя следует использовать точку, что должно приводить к двум разделениям. Если так, то это означает, что на входе имеется разделитель, состоящий из одной точки.
if (splitCounts == 2){ ... }
Если количество разделений равно 1, это означает, что во входном значении отсутствует точка и, следовательно, оно не может быть принято. В этом случае информируем об ошибке и устанавливаем логическую переменную по имени isInputValid в значение false.
else if (splitCounts == 1){ Print("ERROR: YOUR INPUT MUST CONTAIN DECIMAL POINTS"); isInputValid = false; }
Если до сих пор ни одно из условий не выполнено, это означает, что входные данные имеют разделитель с количеством точек больше 1, и это неправильно. Поэтому сообщаем об ошибке и устанавливаем флажок валидности входных данных в значение false.
else { Print("ERROR: YOU CAN NOT HAVE MORE THAN ONE DECIMAL POINT IN INPUT"); isInputValid = false; }
Если мы введем недопустимое значение с разделителем из 2 точек, вот какие выходные данные получим в журнале эксперта.

Для проверки нечисловых символов во входных данных придется пройтись по каждому из двух разделений и оценить каждый символ по отдельности. Чтобы проще было достичь этой цели, требуется цикл for.
if (StringLen(lots_Parts_Array[0]) > 0){ // ... }
Сначала убедимся, что первая строка с индексом 0 в массиве хранения не пустая, то есть в том случае, когда длина строки больше 0. Функция StringLen используется для получения количества символов в строке. Если количество символов в строке меньше или равно 0, это означает, что подстрока пуста, а входное значение уже недопустимо.
else { Print("ERROR: PART 1 (LEFT HAND SIDE) IS EMPTY"); isInputValid = false; }
Для визуализации ошибки ниже представлено, что получится, если оставить левую часть разделителя пустой.

Для проверки на наличие нечисловых символов воспользуемся циклом for, как показано ниже.
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; } }
Определим строковую переменную с именем split, и именно в ней сохраним нашу первую подстроку в массиве хранения. Затем перебираем все символы в подстроке. Для выбранного символа получаем его код, используя StringGetCharacter — функцию, которая возвращает значение символа, расположенного в указанной позиции строки, и сохраняет код символа в короткой беззнаковой переменной с именем symbol_code. Для получения фактического обозначения символа используем строковую функцию substring. Наконец, воспользуемся оператором if, чтобы проверить, входит ли полученный код в числовые коды, а если нет, то это означает, что перед нами нечисловой символ. Поэтому мы сообщаем об ошибке, устанавливаем флаг валидности входных данных в значение false и досрочно прерываем цикл. Если же нет, это означает, что все символы являются числовыми значениями, а наши входные данные будут по-прежнему валидными, для чего и были инициализированы.
bool isInputValid = true;
Как вы могли заметить, числовой диапазон от 48 до 57 считается диапазоном кодов числовых символов. Что ж, посмотрим, почему. Согласно таблице ASCII эти числовые символы соответствуют десятичной системе счисления, начинающейся с 48 для символа 0 и заканчивающейся 57 для символа 9.

Продолжение приведено ниже.

Та же логика применима и ко второй части разделенной строки, то есть к подстроке с права от разделителя. Ее исходный код приведен ниже.
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; }
Чтобы убедиться в возможности отличить числовой символ от нечислового, рассмотрим пример.

Можно видеть, что при добавлении прописной буквы «А», код которой 65, вернем ошибку, указывающую на недействительность входных данных. В этом примере мы использовали букву «А», поскольку код ее символа можно легко найти на представленных изображениях. Это может быть любой другой символ. Теперь снова используем флаг валидности входных данных, чтобы установить допустимое текстовое значение в рассматриваемое поле редактирования.
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); }
Если значение флага валидности входных данных равно true, информируем об успешном исполнении и установим текстовое значение в качестве исходных пользовательских входных данных, поскольку оно не имеет каких-либо расхождений. Снова установим черный цвет текста и белый цвет фона кнопки. Как правило, это исходные свойства поля редактирования. Если выходные данные равны false, это означает, что значение пользовательских входных данных содержит ошибки и его нельзя использовать для торговых операций.
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); }
Следовательно, сообщаем об ошибке и устанавливаем текстовое значение на Error. Для максимального привлечения внимания пользователя мы установили цвет текста белым, а цвет фона красным — броское сочетание облегчает для пользователя распознавание наличия ошибки. После компиляции получаем следующие результаты.

На этом этапе автоматизация большинства компонентов панели завершена. Единственные, оставшиеся неучтенными, — это перемещение раскрывающегося списка и эффект наведения мыши на кнопку. Всех их необходимо учитывать при движении мыши на графике, тогда будет учтен идентификатор события CHARTEVENT_MOUSE_MOVE. Для отслеживания движения мыши нам понадобится включить логику обнаружения движений мыши на графике в экземпляре инициализации эксперта, и это достигается посредством приведенной ниже логики.
//--- enable CHART_EVENT_MOUSE_MOVE detection ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
Начнем с самого простого — с эффекта наведения. Получаем событие, уогда мышь перемещается на графике после включения процедуры ее обнаружения.
else if (id==CHARTEVENT_MOUSE_MOVE){ ... }
Для определения местоположения мыши в пределах графика нам понадобится получить ее координаты, то есть ее местоположение по осям x и y соответственно, а также ее состояние, то есть — находится она в движении или статична.
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)
Здесь объявляем переменную целочисленного типа данных по имени mouse_X для хранения расстояния мыши по оси x или, точнее, по шкале даты и времени. Опять же, получаем параметр double, сохраняем его значение в параметре mouse_Y и, наконец, параметр string — в переменную mouse_State. В конце приведем их к целым числам. Нам понадобятся начальные координаты целевого элемента, поэтому определим их с помощью приведенного ниже фрагмента кода.
//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;
Статическая логическая переменная prevMouseInside объявляется, чтобы отслеживать, находилась ли мышь ранее внутри области этой кнопки. Логическая переменна isMouseInside будет сохранять текущее состояние нашей мыши относительно кнопки, а все переменные инициализируются с флагом false. Чтобы определить, находится ли мышь внутри области кнопки, используем условный оператор.
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; }
Условный оператор проверяет, находится ли курсор мыши в данный момент внутри области кнопки. Если это так, значение isMouseInside устанавливается на true, указывая, что мышь внутри курсора, а в противном случае логическая переменная будет иметь значение false, если условия не выполняются. Технически, для того чтобы курсор мыши можно было считать расположенным внутри области кнопки должны соблюдаться четыре условия. Разберем каждое условие для дальнейшего понимания.
- mouse_X >= XDistance_Hover_Btn: проверяет, будет ли значение координаты X мыши (mouse_X) больше или равно значению левой границы кнопки (XDistance_Hover_Btn).
- mouse_X <= XDistance_Hover_Btn + XSize_Hover_Btn: проверяет, будет ли значение координаты X меньше или равно значению правой границы кнопки (сумма XDistance_Hover_Btn и ширины кнопки XSize_Hover_Btn).
- mouse_Y >= YDistance_Hover_Btn: аналогичным образом проверяет, будет ли значение координаты Y (mouse_Y) больше или равно значению верхней границы кнопки (YDistance_Hover_Btn).
- mouse_Y <= YDistance_Hover_Btn + YSize_Hover_Btn: проверяет, будет ли значение координаты 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); }
Если логическая переменная имеет значение true, это означает, что мышь вошла в область кнопки. Информируем об этом экземпляре с помощью оператора печати. Затем сменим цвет метки кнопки, а также ее фон. Иначе, если переменная имеет значение false, это будет означать, что курсор мыши раньше был внутри области мыши и только что покинул ее. Таким образом, сбрасываем цвета до значений по умолчанию. Ниже приведен фрагмент кода, отвечающий за эту логику.
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, которая обновляет отображение графика и отражает актуализированный внешний вид кнопки. Наконец, переменная prevMouseInside обновляется, чтобы соответствовать текущему состоянию мыши (isMouseInside). Это гарантирует, что при следующем срабатывании события программа сможет сравнить новое состояние с предыдущим.
ChartRedraw(0);//// Redraw the chart to reflect the changes prevMouseInside = isMouseInside;
Полный код, ответственный за создания эффекта наведения курсора на кнопку, выглядит следующим образом:
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
Для инициализации переменных хранения объявляем статические переменные типа данных и инициализируем их значением 0, как указано ниже. Они объявлены статическими, поскольку нам нужно хранить соответствующие размеры и расстояния для справки, когда компоненты панели находятся в движении. Кроме того, нам понадобятся начальные расстояния между элементами, что достигается с помощью приведенного ниже фрагмента кода.
//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);
Здесь мы просто используем функцию ObjectGetInteger для извлечения расстояний элементов, перемещаемых вместе с курсором. Однако обратите внимание, что мы еще получаем размер иконки, которая будет использоваться при перемещении панели. Причина, по которой нам нужны размеры, как и в случае с логикой эффекта наведения, состоит в том, что мы можем определить, когда происходит нажатие на курсоре мыши в области иконки, чтобы мы могли начать движение. Затем нам нужно зафиксировать начальную информацию о щелчке мыши и сохранить расстояния до перемещаемых объектов.
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 } }
Используем условный оператор для проверки двух условий. Первое, prevMouseClickState == false, чтобы гарантировать, что ранее не было нажатия на левую кнопку мыши, и второе, mouse_State == 1, для проверки, не находится ли мышь в данный момент в нажатом состоянии (button down). Если оба условия выполнены, сохраняем координаты X и Y мыши, а также расстояния до объекта. Наконец, проверяем, находится ли мышь в пределах области иконки перетаскивания, и если да, устанавливаем состояние перемещения на true, что означает — сы можем начать перемещение компонентов панели. Для облегчения понимания разберём эти четыре условия:
- mouse_X >= XDistance_Drag_Icon: проверяе, будет ли значение координаты X мыши (mouse_X) больше или равно левой границе области иконки перетаскивания (XDistance_Drag_Icon).
- mouse_X <= XDistance_Drag_Icon + XSize_Drag_Icon: аналогичным образом убеждаемся, что значение координаты X меньше или равно правой границе области иконки перетаскивания (сумма XDistance_Drag_Icon и ширины иконки, XSize_Drag_Icon).
- mouse_Y >= YDistance_Drag_Icon: проверяет, будет ли значение координаты Y мыши (mouse_Y) больше или равно верхней границе области иконки перетаскивания (YDistance_Drag_Icon).
- mouse_Y <= YDistance_Drag_Icon + YSize_Drag_Icon: сходным образом убеждаемся, что значение координаты Y меньше или равно нижней границе области иконки перетаскивания (сумма YDistance_Drag_Icon и высоты иконки, YSize_Drag_Icon).
В случае выполнения всех четырех условий (то есть, если мышь находится в пределах заданной области иконки перетаскивания), устанавливаем переменную movingState в значение true. На этом этапе, если состояние перемещения равно 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); ... ChartRedraw(0); }
Здесь для отключения флажка прокрутки графика воспользуемся функцией ChartSetInteger. Это гарантирует, что при перемещении мыши график не будет прокручиваться по горизонтали. Таким образом, вместе с указанными объектами будет перемещаться только курсор мыши. Наконец, установим новые расстояния до объекта относительно текущих координат мыши и перерисуем график, чтобы изменения вступили в силу. Короче говоря, получим вот что:
![]()
Теперь можно видеть, что мы сумеем перетаскивать иконку. Однако нам, кроме того, понадобится перетащить его вместе с другими компонентами панели. Таким образом, применима та же логика.
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);
Добавление логики перетаскивания других элементов гарантирует, что во время перемещения иконки перетаскивания будут перемещаться и прочие компоненты панели. После компиляции получаем следующее:

Это был успех. Можно видеть, что все компоненты панели перемещаются вместе с курсором мыши. Однако есть небольшая проблема, которую нам необходимо устранить. Если отпустить кнопку мыши, находящейся не в нажатом состоянии, компоненты продолжают перемещаться по мере перемещения курсора. Чтобы вывести панель из состояния перемещения, нам нужно установить состояние в значение false в случае, если кнопка мыши не нажата.
if (mouse_State == 0){ movingState = false; ChartSetInteger(0,CHART_MOUSE_SCROLL,true); }
Если состояние мыши равно нулю, это означает, что левая кнопка мыши отпущена и, следовательно, устанавливаем состояние перемещения на false, указывая, что нам не нужно больше перемещать компоненты панели. Позже мы включим событие прокрутки графика, установив флаг в значение true. Наконец, установим предшествующее состояние мыши на текущее состояние.
prevMouseClickState = mouse_State;
Окончательный исходный код, ответственный за автоматизацию эффекта наведения и перемещение панели, выглядит следующим образом:
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; }
Вкратце, вот чего мы добились.

Это было здорово. Мы только что вдохнули жизнь в нашу панель графического интерфейса, и теперь она интерактивна и хорошо реагирует на воздействия. У нее есть эффекты наведения, щелчки мыши, обновление данных в реальном времени, и она реагирует на движения мыши.
Заключение
В заключение, на основе реализации статьи, можно сказать, что интеграция динамических функций в панель графического интерфейса языка MetaQuotes Language 5 (MQL5) существенно расширяет возможности пользователей, делая ее более интерактивной и функциональной. Добавление эффектов наведения на кнопки создает визуально привлекательный интерфейс, интуитивно реагирующий на действия пользователя. Обновление цен bid и ask в реальном времени обеспечивает трейдерам возможность получения самой актуальной рыночной информации, что позволяет им быстро принимать обоснованные решения. Кликабельные кнопки для исполнения ордеров на покупку и продажу, а также функция закрытия позиций и ордеров оптимизируют торговые операции, позволяя пользователям быстро реагировать на изменения рынка.
Кроме того, реализация перемещаемых подпанелей и выпадающих списков добавляет интерфейсу дополнительный уровень настройки и гибкости. Трейдеры могут организовывать свое рабочее пространство в соответствии со своими предпочтениями, повышая общую эффективность торговли. Функция выпадающих списков обеспечивает удобный доступ к различным опциям, не загромождая основной интерфейс, что способствует формированию более качественной и лучше организованной торговой среды. В целом эти усовершенствования преобразуют панель графического интерфейса MQL5 в надежный и удобный инструмент, отвечающий потребностям современных трейдеров и в конечном итоге улучшающий их торговлю и ее результаты. Представленные знания трейдеры могут использовать для создания более сложных и привлекательных панелей графического интерфейса, которые также улучшат их опыт торговли. Надеемся, что вы сочтете эту статью достаточно подробной, объективно изложенной и несложной для понимания и изучения. С чем вас всех и поздравляем!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15263
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка системы репликации (Часть 65): Нажатие кнопки воспроизведения в сервисе (VI)
Нейросети в трейдинге: Мультизадачное обучение на основе модели ResNeXt
MQL5-советник, интегрированный в Telegram (Часть 1): Отправка сообщений из MQL5 в Telegram
Биологический нейрон для прогнозирования финансовых временных рядов
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Отличная работа, спасибо!
@Clemence Benjamin спасибо за отзыв и признание. Это очень мило с вашей стороны. Добро пожаловать.
Отличная работа, спасибо, что поделились.
Спассибо, классная штука, можно делать частичное закрытие на неттингово счете ?
А то открывается противоложный ордер...