English 中文 Deutsch 日本語
preview
Трейдинг с экономическим календарем MQL5 (Часть 10): Перетаскиваемая панель и hover-эффекты на кнопках

Трейдинг с экономическим календарем MQL5 (Часть 10): Перетаскиваемая панель и hover-эффекты на кнопках

MetaTrader 5Трейдинг |
49 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В данной статье мы продолжаем серию, посвященную экономическому календарю MQL5. Сегодня мы добавим возможность перетаскивать панель, а также эффекты при наведении мыши на кнопки для повышения удобства работы с новостями, обеспечивая интуитивную навигацию и интуитивно понятную работу. За основу возьмем версию из Части 9, в которой мы добавили динамический скроллбар, и улучшим адаптивность пользовательского интерфейса, добавим возможность перемещать календарь на графике, а также улучшим функционал кнопок, чтобы было еще удобнее пользоваться экономическим календарем как в режиме реальной торговли, так и при работе в тестере стратегий. Структура статьи включает следующие темы:

  1. Добавление функциональности для перетаскивания панели на графике
  2. Реализация средствами MQL5
  3. Тестирование и проверка
  4. Заключение

Давайте рассмотрим изменения.


Добавление функциональности для перетаскивания панели на графике

Для повышения удобства работы с экономическим календарем MQL5 мы реализуем перетаскиваемую панель, позволяющую изменять положение интерфейса на графике, а также динамически позиционируемый скроллбар для навигации по новостям. Наша цель — создать гибкий инструмент, который адаптируется к различным компоновкам графика. Мы избавимся от фиксированного позиционирования, которое было в предыдущих версиях и добавим возможность перемещать панель, события и скроллбар. Вот как мы это будем делать:

  • Проектирование перетаскиваемой панели: мы реализуем систему отслеживания кликов мыши в области заголовка, позволяющую перетаскивать всю панель. Все элементы интерфейса будут обновлять свои координаты в реальном времени, чтобы вся панель полностью перемещалась плавно.
  • Динамическое позиционирование срколлбара: для этого переведем полосу прокрутки на относительные координаты, привязанные к позиции панели, чтобы она оставалась функциональной и корректно расположенной во время перетаскивания.
  • Ограничения границ графика: введем ограничения, которые будут удерживать панель в пределах видимой области графика, предотвращая ее выход за пределы экрана и обеспечивая постоянную доступность.
  • Согласованное перемещение элементов: новости, кнопки фильтрации и торговые метки будут перемещаться синхронно с панелью, формируя единый профессиональный интерфейс.

Наш стратегический подход превратит панель в гибкий инструмент, который можно размещать в удобном месте, чтобы улучшать обзор графика и взаимодействие с ним. Вкратце, именно этого мы стремимся достичь.

План разработки


Реализация средствами MQL5

Для внедрения улучшений в MQL5 нам сначала необходимо определить дополнительные функции, которые позволят добавить динамическую интерактивность. Предлагаю двигаться от простого к сложному — от состояний наведения мыши к состояниям перетаскивания. Начнем с определения функции void, которая будет определять состояние наведения кнопки в зависимости от положения курсора на графике и обновлять ее визуальное состояние, как правило, затемняя цвет (при желании можно задать собственные цвета для состояния hover). Сначала реализуем функцию, отвечающую за затемнение цвета.

//+------------------------------------------------------------------+
//| Helper function to darken a color for hover effect               |
//+------------------------------------------------------------------+
color ColorToDarken(color clr) {
   int r = (clr & 0xFF);
   int g = ((clr >> 8) & 0xFF);
   int b = ((clr >> 16) & 0xFF);
   r = MathMax(0, r - 50);
   g = MathMax(0, g - 50);
   b = MathMax(0, b - 50);
   return (color)((b << 16) | (g << 8) | r);
}

Мы определяем функцию ColorToDarken для генерации более темной версии исходного цвета. ЕЕ мы будем использовать для усиления визуального эффекта при наведении курсора на кнопки. Входной параметр clr представляет собой одно числовое значение, объединяющее компоненты красного, зеленого и синего (каждая в диапазоне от 0 до 255), они формируют конкретный цвет. Для разделения этих компонентов используем побитовые операции, позволяющие работать с отдельными битами внутри clr.

Для извлечения красной компоненты r используется оператор & (побитовое AND) с 0xFF — шестнадцатеричным значением, равным 255 в десятичной системе, которое выступает в роли фильтра и выделяет младшие 8 бит clr, соответствующие интенсивности красного. Для получения зеленой компоненты g значение clr сдвигается вправо на 8 бит с помощью оператора >>, после чего снова применяется & с 0xFF для изоляции компоненты. Аналогично синяя компонента b извлекается путем сдвига clr вправо на 16 бит и применения & с 0xFF. Вот как записываются значения в шестнадцатеричной системе:

255 В шестнадцатеричном формате 0xFF

Для затемнения цвета мы вычитаем 50 из каждой компоненты r, g и b, чтобы уменьшить их интенсивность, и используем функцию MathMax, чтобы ни одно значение не стало меньше 0, поскольку цвет не может быть отрицательным. Затем объединяем затемненные компоненты обратно в единое цветовое значение: сдвигаем b влево на 16 бит с помощью оператора <<, g — на 8 бит и объединяем их с r при помощи оператора | (побитовое OR). Возвращаемое значение color применяется к кнопкам, создавая затемненный hover-эффект и делая интерфейс более интуитивным. Теперь эту функцию можно использовать для динамического форматирования hover-состояний, которые будут использоваться при наведении мыши.

//+------------------------------------------------------------------+
//| Update hover states for header and buttons                       |
//+------------------------------------------------------------------+
void updateHoverStates(int mouse_x, int mouse_y) {
   // Header hover
   int header_x = (int)ObjectGetInteger(0, HEADER_LABEL, OBJPROP_XDISTANCE);
   int header_y = (int)ObjectGetInteger(0, HEADER_LABEL, OBJPROP_YDISTANCE);
   int header_width = 740;
   int header_height = 30;

   bool is_header_hovered = (mouse_x >= header_x && mouse_x <= header_x + header_width &&
                             mouse_y >= header_y && mouse_y <= header_y + header_height);

   if (is_header_hovered && !header_hovered) {
      ObjectSetInteger(0, MAIN_REC, OBJPROP_BGCOLOR, clrDarkGreen);
      header_hovered = true;
   } else if (!is_header_hovered && header_hovered) {
      ObjectSetInteger(0, MAIN_REC, OBJPROP_BGCOLOR, clrSeaGreen);
      header_hovered = false;
   }
}

Далее реализуем функцию updateHoverStates для добавления hover-эффекта к заголовку. Мы получаем позицию заголовка HEADER_LABEL с помощью функции ObjectGetInteger, извлекаем координаты header_x и header_y с помощью функций OBJPROP_XDISTANCE и OBJPROP_YDISTANCE, которые образуют область размером 740x30 пикселей. Проверяем, находятся ли mouse_x и mouse_y внутри этой области, и устанавливаем флаг is_header_hovered при необходимости.

Если флаг is_header_hovered равен true, а header_hovered равно false, мы используем функцию ObjectSetInteger для установки цвета фона MAIN_REC (OBJPROP_BGCOLOR) в значение clrDarkGreen и обновляем header_hovered в true. Если же курсор покинул область и header_hovered было true, возвращаем цвет clrSeaGreen и устанавливаем header_hovered в false. Аналогичную логику применяем к другим кнопкам.

// FILTER_CURR_BTN hover
int curr_btn_x = (int)ObjectGetInteger(0, FILTER_CURR_BTN, OBJPROP_XDISTANCE);
int curr_btn_y = (int)ObjectGetInteger(0, FILTER_CURR_BTN, OBJPROP_YDISTANCE);
int curr_btn_width = 110;
int curr_btn_height = 26;

bool is_curr_btn_hovered = (mouse_x >= curr_btn_x && mouse_x <= curr_btn_x + curr_btn_width &&
                            mouse_y >= curr_btn_y && mouse_y <= curr_btn_y + curr_btn_height);

if (is_curr_btn_hovered && !filter_curr_hovered) {
   ObjectSetInteger(0, FILTER_CURR_BTN, OBJPROP_BGCOLOR, clrDarkGray);
   filter_curr_hovered = true;
} else if (!is_curr_btn_hovered && filter_curr_hovered) {
   ObjectSetInteger(0, FILTER_CURR_BTN, OBJPROP_BGCOLOR, clrBlack);
   filter_curr_hovered = false;
}

// FILTER_IMP_BTN hover
int imp_btn_x = (int)ObjectGetInteger(0, FILTER_IMP_BTN, OBJPROP_XDISTANCE);
int imp_btn_y = (int)ObjectGetInteger(0, FILTER_IMP_BTN, OBJPROP_YDISTANCE);
int imp_btn_width = 120;
int imp_btn_height = 26;

bool is_imp_btn_hovered = (mouse_x >= imp_btn_x && mouse_x <= imp_btn_x + imp_btn_width &&
                           mouse_y >= imp_btn_y && mouse_y <= imp_btn_y + imp_btn_height);

if (is_imp_btn_hovered && !filter_imp_hovered) {
   ObjectSetInteger(0, FILTER_IMP_BTN, OBJPROP_BGCOLOR, clrDarkGray);
   filter_imp_hovered = true;
} else if (!is_imp_btn_hovered && filter_imp_hovered) {
   ObjectSetInteger(0, FILTER_IMP_BTN, OBJPROP_BGCOLOR, clrBlack);
   filter_imp_hovered = false;
}

// FILTER_TIME_BTN hover
int time_btn_x = (int)ObjectGetInteger(0, FILTER_TIME_BTN, OBJPROP_XDISTANCE);
int time_btn_y = (int)ObjectGetInteger(0, FILTER_TIME_BTN, OBJPROP_YDISTANCE);
int time_btn_width = 70;
int time_btn_height = 26;

bool is_time_btn_hovered = (mouse_x >= time_btn_x && mouse_x <= time_btn_x + time_btn_width &&
                            mouse_y >= time_btn_y && mouse_y <= time_btn_y + time_btn_height);

if (is_time_btn_hovered && !filter_time_hovered) {
   ObjectSetInteger(0, FILTER_TIME_BTN, OBJPROP_BGCOLOR, clrDarkGray);
   filter_time_hovered = true;
} else if (!is_time_btn_hovered && filter_time_hovered) {
   ObjectSetInteger(0, FILTER_TIME_BTN, OBJPROP_BGCOLOR, clrBlack);
   filter_time_hovered = false;
}

// CANCEL_BTN hover
int cancel_btn_x = (int)ObjectGetInteger(0, CANCEL_BTN, OBJPROP_XDISTANCE);
int cancel_btn_y = (int)ObjectGetInteger(0, CANCEL_BTN, OBJPROP_YDISTANCE);
int cancel_btn_width = 50;
int cancel_btn_height = 30;

bool is_cancel_btn_hovered = (mouse_x >= cancel_btn_x && mouse_x <= cancel_btn_x + cancel_btn_width &&
                              mouse_y >= cancel_btn_y && mouse_y <= cancel_btn_y + cancel_btn_height);

if (is_cancel_btn_hovered && !cancel_hovered) {
   ObjectSetInteger(0, CANCEL_BTN, OBJPROP_BGCOLOR, clrDarkRed);
   cancel_hovered = true;
} else if (!is_cancel_btn_hovered && cancel_hovered) {
   ObjectSetInteger(0, CANCEL_BTN, OBJPROP_BGCOLOR, clrRed);
   cancel_hovered = false;
}

// CURRENCY_BTNS hover
int curr_size = 51, button_height = 22, spacing_x = 0, spacing_y = 3, max_columns = 4;
for (int i = 0; i < ArraySize(curr_filter); i++) {
   int row = i / max_columns;
   int col = i % max_columns;
   int x_pos = panel_x + 525 + col * (curr_size + spacing_x);
   int y_pos = panel_y + 33 + row * (button_height + spacing_y);
   bool is_curr_hovered = (mouse_x >= x_pos && mouse_x <= x_pos + curr_size &&
                           mouse_y >= y_pos && mouse_y <= y_pos + button_height);
   string btn_name = CURRENCY_BTNS+IntegerToString(i);
   if (is_curr_hovered && !currency_btns_hovered[i]) {
      ObjectSetInteger(0, btn_name, OBJPROP_BGCOLOR, clrLightGray);
      currency_btns_hovered[i] = true;
   } else if (!is_curr_hovered && currency_btns_hovered[i]) {
      ObjectSetInteger(0, btn_name, OBJPROP_BGCOLOR, clrNONE);
      currency_btns_hovered[i] = false;
   }
}

// IMPACT_LABEL buttons hover
int impact_size = 100;
for (int i = 0; i < ArraySize(impact_labels); i++) {
   int x_pos = panel_x + 90 + impact_size * i;
   int y_pos = panel_y + 55;
   bool is_impact_hovered = (mouse_x >= x_pos && mouse_x <= x_pos + impact_size &&
                             mouse_y >= y_pos && mouse_y <= y_pos + 25);
   string btn_name = IMPACT_LABEL+string(i);
   color normal_color = clrBlack;
   if (impact_labels[i] == "None") normal_color = clrBlack;
   else if (impact_labels[i] == "Low") normal_color = clrYellow;
   else if (impact_labels[i] == "Medium") normal_color = clrOrange;
   else if (impact_labels[i] == "High") normal_color = clrRed;
   color hover_color = normal_color == clrBlack ? clrDarkGray : ColorToDarken(normal_color);
   if (is_impact_hovered && !impact_btns_hovered[i]) {
      ObjectSetInteger(0, btn_name, OBJPROP_BGCOLOR, hover_color);
      impact_btns_hovered[i] = true;
   } else if (!is_impact_hovered && impact_btns_hovered[i]) {
      ObjectSetInteger(0, btn_name, OBJPROP_BGCOLOR, normal_color);
      impact_btns_hovered[i] = false;
   }
}

Мы расширяем функцию updateHoverStates для добавления hover-эффектов к нескольким кнопкам, которые дают визуальную обратную связь при взаимодействии пользователя. Для кнопки FILTER_CURR_BTN с помощью ObjectGetInteger получаем координаты curr_btn_x и curr_btn_y через OBJPROP_XDISTANCE и OBJPROP_YDISTANCE, получается область 110x26 пикселей. Проверяем, находятся ли значения mouse_x и mouse_y внутри нее, чтобы установить флаг is_curr_btn_hovered.

Если is_curr_btn_hovered равно true, а filter_curr_hovered равно false, устанавливаем цвет фона FILTER_CURR_BTN (OBJPROP_BGCOLOR) в clrDarkGray через функцию ObjectSetInteger и обновляем состояние filter_curr_hovered в true. Иначе возвращаем цвет clrBlack и устанавливаем флаг filter_curr_hovered в false.

Аналогичная логика применяется к кнопкам FILTER_IMP_BTN (120x26 пикселей) и FILTER_TIME_BTN (70x26 пикселей) - для них обновляются состояния filter_imp_hovered и filter_time_hovered соответственно. Для кнопки отмены CANCEL_BTN (50x30 пикселей) цвет переключается между clrDarkRed и clrRed в зависимости от состояний is_cancel_btn_hovered и cancel_hovered. Для кнопок валют CURRENCY_BTNS выполняется цикл по каждой кнопке, вычисляются позиции с использованием panel_x и panel_y, определяется состояние is_curr_hovered и осуществляется переключение между clrLightGray и clrNONE для currency_btns_hovered[i].

Для кнопок важности IMPACT_LABEL задается цвет normal_color в зависимости от уровня важности (например, clrYellow для Low), вычисляется цвет наведения hover_color через ColorToDarken и выполняется переключение цвета на основе состояний is_impact_hovered и impact_btns_hovered[i]. Чтобы изменения вступили в силу, функция вызывается в обработчике события OnChartEvent.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   // Update hover states
   if (id == CHARTEVENT_MOUSE_MOVE) {
      updateHoverStates(mouse_x, mouse_y);
   }
}

В обработчике OnChartEvent мы сначала получаем координаты мыши mouse_x и mouse_y из параметров lparam и dparam, а также состояние мыши mouse_state из sparam, определяющее нажатие или отпускание кнопки. Когда id равно CHARTEVENT_MOUSE_MOVE, вызывается функция updateHoverStates с передачей значений mouse_x и mouse_y для проверки наведения на заголовок или кнопки и обновления их визуального состояния. Так мы получаем удобную обратную связь при наведении для интерактивных элементов панели. Далее необходимо обрабатывать клики для обновления элементов и определять состояние перемещения элементов.

if (id == CHARTEVENT_OBJECT_CLICK) {
   UpdateFilterInfo();
   CheckForNewsTrade();
   if (sparam == CANCEL_BTN) {
      isDashboardUpdate = false;
      destroy_Dashboard();
   }
   if (sparam == FILTER_CURR_BTN) {
      bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
      enableCurrencyFilter = btn_state;
      if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableCurrencyFilter);
      string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
      color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
      ObjectSetString(0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text);
      ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_COLOR,filter_curr_txt_color);
      if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
      // Recalculate scrollbar
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         createRecLabel(SCROLL_LEADER, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         updateSliderPosition();
         updateButtonColors();
      }
      if (debugLogging) Print("Success. Changes updated! State: "+(string)enableCurrencyFilter);
      ChartRedraw(0);
   }
   if (sparam == FILTER_IMP_BTN) {
      bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
      enableImportanceFilter = btn_state;
      if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableImportanceFilter);
      string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
      color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
      ObjectSetString(0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text);
      ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color);
      if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
      // Recalculate scrollbar
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         createRecLabel(SCROLL_LEADER, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         updateSliderPosition();
         updateButtonColors();
      }
      if (debugLogging) Print("Success. Changes updated! State: "+(string)enableImportanceFilter);
      ChartRedraw(0);
   }
   if (sparam == FILTER_TIME_BTN) {
      bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
      enableTimeFilter = btn_state;
      if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableTimeFilter);
      string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
      color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
      ObjectSetString(0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text);
      ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_COLOR,filter_time_txt_color);
      if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
      // Recalculate scrollbar
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         createRecLabel(SCROLL_LEADER, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         updateSliderPosition();
         updateButtonColors();
      }
      if (debugLogging) Print("Success. Changes updated! State: "+(string)enableTimeFilter);
      ChartRedraw(0);
   }
   if (StringFind(sparam,CURRENCY_BTNS) >= 0) {
      string selected_curr = ObjectGetString(0,sparam,OBJPROP_TEXT);
      if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr);
      bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
      if (btn_state == false) {
         if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
         for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
            if (curr_filter_selected[i] == selected_curr) {
               for (int j = i; j < ArraySize(curr_filter_selected) - 1; j++) {
                  curr_filter_selected[j] = curr_filter_selected[j + 1];
               }
               ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - 1);
               if (debugLogging) Print("Removed from selected filters: ", selected_curr);
               break;
            }
         }
      } else {
         if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
         bool already_selected = false;
         for (int j = 0; j < ArraySize(curr_filter_selected); j++) {
            if (curr_filter_selected[j] == selected_curr) {
               already_selected = true;
               break;
            }
         }
         if (!already_selected) {
            ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + 1);
            curr_filter_selected[ArraySize(curr_filter_selected) - 1] = selected_curr;
            if (debugLogging) Print("Added to selected filters: ", selected_curr);
         } else {
            if (debugLogging) Print("Currency already selected: ", selected_curr);
         }
      }
      if (debugLogging) Print("SELECTED ARRAY SIZE = ",ArraySize(curr_filter_selected));
      if (debugLogging) ArrayPrint(curr_filter_selected);
      if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
      // Recalculate scrollbar
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         createRecLabel(SCROLL_LEADER, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         updateSliderPosition();
         updateButtonColors();
      }
      if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
      ChartRedraw(0);
   }
   if (StringFind(sparam, IMPACT_LABEL) >= 0) {
      string selected_imp = ObjectGetString(0, sparam, OBJPROP_TEXT);
      ENUM_CALENDAR_EVENT_IMPORTANCE selected_importance_lvl = get_importance_level(impact_labels,allowed_importance_levels,selected_imp);
      if (debugLogging) Print("BTN NAME = ", sparam, ", IMPORTANCE LEVEL = ", selected_imp,"(",selected_importance_lvl,")");
      bool btn_state = ObjectGetInteger(0, sparam, OBJPROP_STATE);
      color color_border = btn_state ? clrNONE : clrBlack;
      if (btn_state == false) {
         if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
         for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
            if (impact_filter_selected[i] == selected_imp) {
               for (int j = i; j < ArraySize(imp_filter_selected) - 1; j++) {
                  imp_filter_selected[j] = imp_filter_selected[j + 1];
                  impact_filter_selected[j] = impact_filter_selected[j + 1];
               }
               ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - 1);
               ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - 1);
               if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
               break;
            }
         }
      } else {
         if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
         bool already_selected = false;
         for (int j = 0; j < ArraySize(imp_filter_selected); j++) {
            if (impact_filter_selected[j] == selected_imp) {
               already_selected = true;
               break;
            }
         }
         if (!already_selected) {
            ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + 1);
            ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1);
            imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl;
            impact_filter_selected[ArraySize(impact_filter_selected) - 1] = selected_imp;
            if (debugLogging) Print("Added to selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
         } else {
            if (debugLogging) Print("Importance level already selected: ", selected_imp,"(",selected_importance_lvl,")");
         }
      }
      if (debugLogging) Print("SELECTED ARRAY SIZE = ", ArraySize(imp_filter_selected)," >< ",ArraySize(impact_filter_selected));
      if (debugLogging) ArrayPrint(imp_filter_selected);
      if (debugLogging) ArrayPrint(impact_filter_selected);
      if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
      ObjectSetInteger(0,sparam,OBJPROP_BORDER_COLOR,color_border);
      // Recalculate scrollbar
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         createRecLabel(SCROLL_LEADER, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         updateSliderPosition();
         updateButtonColors();
      }
      if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
      ChartRedraw(0);
   }
   // Scrollbar button clicks
   if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
      scrollUp();
      updateButtonColors();
      if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
      ChartRedraw(0);
   }
   if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
      scrollDown();
      updateButtonColors();
      if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
      ChartRedraw(0);
   }
}

Для обработки кликов объектов при id равном CHARTEVENT_OBJECT_CLICK сначала вызываются функции UpdateFilterInfo и CheckForNewsTrade для обновления состояний фильтров и проверки торговых условий. Если sparam равно значению CANCEL_BTN, устанавливаем isDashboardUpdate в значение false и вызываем функцию destroy_Dashboard для закрытия панели. Для кнопок FILTER_CURR_BTN, FILTER_IMP_BTN или FILTER_TIME_BTN получаем состояние кнопки btn_state с помощью функции ObjectGetInteger и OBJPROP_STATE, обновляем значение enableCurrencyFilter, enableImportanceFilter или enableTimeFilter и корректируем текст и цвет кнопки с помощью ObjectSetString и ObjectSetInteger (OBJPROP_TEXT и OBJPROP_COLOR), например clrLime для включенного состояния и clrRed для отключенного. Затем вызываем функцию update_dashboard_values и пересоздаем скроллбар с помощью функций createRecLabel, createLabel и createButton для элементов SCROLL_LEADER и SCROLL_SLIDER, позиционируя их с использованием panel_x и panel_y, и обновляем состояние с помощью функций через updateSliderPosition и updateButtonColors.

Для кнопок валют CURRENCY_BTNS получаем выбранную валюту selected_curr с помощью ObjectGetString, добавляем или удаляем ее из массива curr_filter_selected при помощи ArrayResize и обновляем панель. Для кнопок важности события IMPACT_LABEL вызываем функцию get_importance_level для сопоставления selected_imp с selected_importance_lvl, обновляем массивы imp_filter_selected и impact_filter_selected, устанавливаем цвет color_border с помощью функции ObjectSetInteger и выполняем обновление панели. Если sparam равно SCROLL_UP_REC или SCROLL_UP_LABEL, вызывается scrollUp; если SCROLL_DOWN_REC или SCROLL_DOWN_LABEL — scrollDown. После этого обновляются цвета через updateButtonColors и выполняется перерисовка графика с помощью функции ChartRedraw. После компиляции и запуска программы получаем следующий результат.

Кнопки с эффектом наведения

НА анимации видно, что реализована корректная визуальная обратная связь при наведении на заданные кнопки. Следующий шаг — реализация более сложной части, а именно перемещения всей панели. Здесь у нас следующая логика.

else if (id == CHARTEVENT_MOUSE_MOVE && isDashboardUpdate) {
   // Handle panel dragging
   int header_x = (int)ObjectGetInteger(0, HEADER_LABEL, OBJPROP_XDISTANCE);
   int header_y = (int)ObjectGetInteger(0, HEADER_LABEL, OBJPROP_YDISTANCE);
   int header_width = 740;
   int header_height = 30;

   if (prev_mouse_state == 0 && mouse_state == 1) { // Mouse button down
      if (mouse_x >= header_x && mouse_x <= header_x + header_width &&
          mouse_y >= header_y && mouse_y <= header_y + header_height) {
         panel_dragging = true;
         panel_drag_x = mouse_x;
         panel_drag_y = mouse_y;
         panel_start_x = panel_x;
         panel_start_y = panel_y;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         if (debugLogging) Print("Panel drag started at x=", mouse_x, ", y=", mouse_y);
      }
   }

   if (panel_dragging && mouse_state == 1) { // Dragging panel
      int dx = mouse_x - panel_drag_x;
      int dy = mouse_y - panel_drag_y;
      panel_x = panel_start_x + dx;
      panel_y = panel_start_y + dy;

      // Ensure panel stays within chart boundaries
      int chart_width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      int chart_height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
      panel_x = MathMax(0, MathMin(panel_x, chart_width - 753)); // 753 = panel width
      panel_y = MathMax(0, MathMin(panel_y, chart_height - 410)); // 410 = panel height

      // Update positions of all panel objects
      ObjectSetInteger(0, MAIN_REC, OBJPROP_XDISTANCE, panel_x);
      ObjectSetInteger(0, MAIN_REC, OBJPROP_YDISTANCE, panel_y);
      ObjectSetInteger(0, SUB_REC1, OBJPROP_XDISTANCE, panel_x + 3);
      ObjectSetInteger(0, SUB_REC1, OBJPROP_YDISTANCE, panel_y + 30);
      ObjectSetInteger(0, SUB_REC2, OBJPROP_XDISTANCE, panel_x + 3 + 5);
      ObjectSetInteger(0, SUB_REC2, OBJPROP_YDISTANCE, panel_y + 30 + 50 + 27);
      ObjectSetInteger(0, HEADER_LABEL, OBJPROP_XDISTANCE, panel_x + 3 + 5);
      ObjectSetInteger(0, HEADER_LABEL, OBJPROP_YDISTANCE, panel_y + 5);
      ObjectSetInteger(0, TIME_LABEL, OBJPROP_XDISTANCE, panel_x + 20);
      ObjectSetInteger(0, TIME_LABEL, OBJPROP_YDISTANCE, panel_y + 35);
      ObjectSetInteger(0, IMPACT_LABEL, OBJPROP_XDISTANCE, panel_x + 20);
      ObjectSetInteger(0, IMPACT_LABEL, OBJPROP_YDISTANCE, panel_y + 55);
      ObjectSetInteger(0, FILTER_LABEL, OBJPROP_XDISTANCE, panel_x + 320);
      ObjectSetInteger(0, FILTER_LABEL, OBJPROP_YDISTANCE, panel_y + 5);
      ObjectSetInteger(0, FILTER_CURR_BTN, OBJPROP_XDISTANCE, panel_x + 380);
      ObjectSetInteger(0, FILTER_CURR_BTN, OBJPROP_YDISTANCE, panel_y + 5);
      ObjectSetInteger(0, FILTER_IMP_BTN, OBJPROP_XDISTANCE, panel_x + 490);
      ObjectSetInteger(0, FILTER_IMP_BTN, OBJPROP_YDISTANCE, panel_y + 5);
      ObjectSetInteger(0, FILTER_TIME_BTN, OBJPROP_XDISTANCE, panel_x + 610);
      ObjectSetInteger(0, FILTER_TIME_BTN, OBJPROP_YDISTANCE, panel_y + 5);
      ObjectSetInteger(0, CANCEL_BTN, OBJPROP_XDISTANCE, panel_x + 692+10);
      ObjectSetInteger(0, CANCEL_BTN, OBJPROP_YDISTANCE, panel_y + 1);

      // Update calendar buttons
      int startX = panel_x + 9;
      for (int i = 0; i < ArraySize(array_calendar); i++) {
         ObjectSetInteger(0, ARRAY_CALENDAR+IntegerToString(i), OBJPROP_XDISTANCE, startX);
         ObjectSetInteger(0, ARRAY_CALENDAR+IntegerToString(i), OBJPROP_YDISTANCE, panel_y + 82);
         startX += buttons[i] + 3;
      }

      // Update impact buttons
      for (int i = 0; i < ArraySize(impact_labels); i++) {
         ObjectSetInteger(0, IMPACT_LABEL+string(i), OBJPROP_XDISTANCE, panel_x + 90 + 100 * i);
         ObjectSetInteger(0, IMPACT_LABEL+string(i), OBJPROP_YDISTANCE, panel_y + 55);
      }

      // Update currency buttons
      int curr_size = 51, button_height = 22, spacing_x = 0, spacing_y = 3, max_columns = 4;
      for (int i = 0; i < ArraySize(curr_filter); i++) {
         int row = i / max_columns;
         int col = i % max_columns;
         int x_pos = panel_x + 525 + col * (curr_size + spacing_x);
         int y_pos = panel_y + 33 + row * (button_height + spacing_y);
         ObjectSetInteger(0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_XDISTANCE, x_pos);
         ObjectSetInteger(0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_YDISTANCE, y_pos);
      }

      // Update scrollbar
      if (scroll_visible) {
         ObjectSetInteger(0, SCROLL_LEADER, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET);
         ObjectSetInteger(0, SCROLL_LEADER, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET);
         ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X);
         ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET);
         ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X);
         ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET - 5);
         ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X);
         ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE);
         ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X);
         ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE - 5);
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X);
         // Y-position of slider is managed by updateSliderPosition()
      }

      // Update event display
      update_dashboard_values(curr_filter_selected, imp_filter_selected);

      // Update trade labels if they exist
      if (ObjectFind(0, "NewsCountdown") >= 0) {
         ObjectSetInteger(0, "NewsCountdown", OBJPROP_XDISTANCE, panel_x);
         ObjectSetInteger(0, "NewsCountdown", OBJPROP_YDISTANCE, panel_y - 33);
      }
      if (ObjectFind(0, "NewsTradeInfo") >= 0) {
         ObjectSetInteger(0, "NewsTradeInfo", OBJPROP_XDISTANCE, panel_x + 305);
         ObjectSetInteger(0, "NewsTradeInfo", OBJPROP_YDISTANCE, panel_y - 28);
      }

      ChartRedraw(0);
      if (debugLogging) Print("Panel moved to x=", panel_x, ", y=", panel_y);
   }

   if (mouse_state == 0) { // Mouse button released
      if (panel_dragging) {
         panel_dragging = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         if (debugLogging) Print("Panel drag stopped.");
         ChartRedraw(0);
      }
   }

}

Мы расширяем функцию OnChartEvent, чтобы реализовать возможность перетаскивания панели, когда isDashboardUpdate равно true и id равно CHARTEVENT_MOUSE_MOVE. Позицию заголовка получаем с помощью функции ObjectGetInteger, чтобы получить параметры "header_x" и "header_y" из HEADER_LABEL с использованием OBJPROP_XDISTANCE и OBJPROP_YDISTANCE, задавая область 740x30 пикселей. Когда предыдущее состояние мыши prev_mouse_state равно 0, а новое mouse_state равно 1 (нажатие кнопки мыши), проверяем, находятся ли mouse_x и mouse_y в пределах заголовка. Если да, устанавливаем параметр panel_dragging в значение true, сохраняем значения координат panel_drag_x, panel_drag_y, panel_start_x и panel_start_y и отключаем прокрутку графика через ChartSetInteger и CHART_MOUSE_SCROLL.

Если состояние перетаскивания panel_dragging равно true и текущее состояние mouse_state равно 1, вычисляем смещение dx и dy, обновляем расположение панели panel_x и panel_y и ограничиваем их с помощью MathMax и MathMin в пределах размеров графика chart_width и chart_height, полученных функцией ChartGetInteger с использованием CHART_WIDTH_IN_PIXELS и CHART_HEIGHT_IN_PIXELS. Затем перепозиционируем все элементы интерфейса, используя функцию ObjectSetInteger с параметрами OBJPROP_XDISTANCE и OBJPROP_YDISTANCE для всех объектов: MAIN_REC, SUB_REC1, SUB_REC2, HEADER_LABEL, TIME_LABEL, IMPACT_LABEL, FILTER_LABEL, FILTER_CURR_BTN, FILTER_IMP_BTN, FILTER_TIME_BTN, CANCEL_BTN, ARRAY_CALENDAR, IMPACT_LABEL и CURRENCY_BTNS, а также обновляем элементы полосы прокрутки (SCROLL_LEADER, SCROLL_UP_REC, SCROLL_UP_LABEL, SCROLL_DOWN_REC, SCROLL_DOWN_LABEL, SCROLL_SLIDER), если scroll_visible равно true — для этого используем panel_x и panel_y с соответствующими смещениями.

Вызываем функцию update_dashboard_values для обновления новостей на панели, перепозиционируем торговые метки NewsCountdown и NewsTradeInfo (при их наличии), используя функцию ObjectFind и выполняем ChartRedraw. Когда mouse_state становится 0 (кнопка мыши отпущена), устанавливаем флаг состояние panel_dragging в false, повторно включаем прокрутку графика через ChartSetInteger и выполняем перерисовку, при необходимости выводя сообщения через Print (если включен параметр debugLogging). После этого можно обрабатывать логику скроллбара, если панель не находится в режиме перетаскивания, чтобы избежать конфликтов событий.

// Scrollbar handling (only if not dragging panel)
if (scroll_visible && !panel_dragging) {
   if (prev_mouse_state == 0 && mouse_state == 1) {
      int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
      int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
      int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
      int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
      // Skip if mouse is over header to prioritize panel dragging
      if (mouse_x >= (panel_x + 3 + 5) && mouse_x <= (panel_x + 3 + 5 + 740) &&
          mouse_y >= (panel_y + 5) && mouse_y <= (panel_y + 5 + 30)) {
          return;
      }
      if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
         moving_state_slider = true;
         mlb_down_x = mouse_x;
         mlb_down_y = mouse_y;
         mlb_down_yd_slider = yd;
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         if (debugLogging) Print("Slider drag started at y=", mouse_y);
      }
   }
   if (moving_state_slider && mouse_state == 1) {
      int delta_y = mouse_y - mlb_down_y;
      int new_y = mlb_down_yd_slider + delta_y;
      int scroll_area_y_min = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE;
      int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
      new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
      ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
      int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
      double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
      int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
      if (new_scroll_pos != scroll_pos) {
         scroll_pos = new_scroll_pos;
         update_dashboard_values(curr_filter_selected, imp_filter_selected);
         updateButtonColors();
         if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
      }
      ChartRedraw(0);
   }
   if (mouse_state == 0) {
      if (moving_state_slider) {
         moving_state_slider = false;
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         if (debugLogging) Print("Slider drag stopped.");
         ChartRedraw(0);
      }
   }
}
prev_mouse_state = mouse_state;

Здесь мы реализуем перетаскивание скроллбара, оно выполняется при значении scroll_visible равном true и panel_dragging равном false, что исключает конфликт с перетаскиванием панели. Когда prev_mouse_state равно 0, а mouse_state равно 1, применяется ранее реализованная логика прокрутки и обработки состояний ползунка. После компиляции мы получаем следующий результат.

Передвижение панели

На анимации видно, что можно наводить курсор на кнопки и перетаскивать панель в пределах границ графика. Нам остается лишь провести полноценное тестирование системы, сделаем это в следующем разделе.


Тестирование и проверка

Мы протестировали обновленную панель, чтобы убедиться, что динамическое перемещение, hover-состояние кнопок и полоса прокрутки работают корректно. В тестировании мы проверяли всю визуальную обратную связь кнопок при наведении, перемещение всех компонентов панели в пределах графика и интеграцию скроллбара в режиме реального времени. Результаты тестирования показаны в компактном формате GIF (Graphics Interchange Format), чтобы наглядно продемонстрировать работу панели.

GIF с результатами тестирования

Как видим, панель функционирует без сбоев.


Заключение

В данной части серии об экономическом календаре MQL5 мы реализовали перетаскиваемую панель и интерактивные эффекты при наведении мыши на кнопки. Надеюсь, навигация по событиям стала удобнее. За основу улучшений мы брали разработки из Части 9 с динамическим скроллбаром и улучшенным дизайном, а добавленный функционал работает как в режиме реальной торговли, так и в тестере, предоставляя надежную платформу для торговых стратегий по новостям. Вы можете использовать эту доработанную панель в качестве основы и адаптировать ее под свои торговые требования.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18241

Прикрепленные файлы |
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Моделирование рынка (Часть 19): Первые шаги на SQL (II) Моделирование рынка (Часть 19): Первые шаги на SQL (II)
Как мы объясняли в первой статье о SQL, нет смысла тратить время на программирование процедур для выполнения того, что уже включено в SQL. Однако, если не знать самых основ, вы не сможете ничего сделать с помощью SQL, чтобы воспользоваться всеми преимуществами, которые предлагает этот инструмент. Поэтому в данной статье мы рассмотрим, как выполнять основные задачи в базах данных.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Возможности Мастера MQL5, которые вам нужно знать (Часть 67): Использование паттернов TRIX и процентного диапазона Уильямса Возможности Мастера MQL5, которые вам нужно знать (Часть 67): Использование паттернов TRIX и процентного диапазона Уильямса
Тройной экспоненциальный осциллятор скользящей средней (Triple Exponential Moving Average Oscillator, TRIX) и осциллятор процентного диапазона Уильямса (Williams Percentage Range Oscillator) — это еще одна пара индикаторов, которые можно использовать совместно в советнике MQL5. Эта пара индикаторов, как и те, которые мы недавно рассматривали, также дополняет друг друга, поскольку TRIX определяет тренд, а процентный диапазон подтверждает уровни поддержки и сопротивления. Как всегда, мы используем Мастер MQL5 для оценки потенциала индикаторов.