Трейдинг с экономическим календарем MQL5 (Часть 9): Расширение интерактивности с новостями через динамический скроллбар и улучшенное отображение
Введение
В данной статье мы продолжаем серию, посвященную экономическому календарю MQL5. Сегодня мы добавим вертикальную динамическую полосу прокрутки (скроллбар) и улучшим отображение для повышения удобства работы с новостями, обеспечивая интуитивную навигацию и надежное представление событий. ЗА основу мы используем версию из Части 8, оптимизированную под тестирование на истории и фильтрацию. Сегодня мы уделим внимание на адаптивном пользовательском интерфейсе (UI) с полосой прокрутки, которая будет визуально показывать кликабельные состояния. Это упростит доступ к новостям как в режиме реальной торговли, так и при работе в тестере стратегий. Структура статьи включает следующие темы:
- Создание динамического скроллбара для удобного просмотра новостей
- Реализация средствами MQL5
- Тестирование и проверка
- Заключение
Давайте рассмотрим изменения.
Создание динамического скроллбара для удобного просмотра новостей
Динамический скроллбар послужит основой для интуитивной навигации по новостям и позволит значительно улучшить взаимодействие пользователя с Экономическим календарем MQL5. Мы разработаем адаптивную вертикальную полосу прокрутки, которая будет визуально отображать кликабельные состояния. Помимо этого, реализуем надежную систему хранения событий для удобного доступа ко всем отфильтрованным новостям. Это превратит панель в гибкий, удобный в использовании инструмент. Также мы уберем ограничение на минимальное количество отображаемых событий и будем выводить все отфильтрованные события, чтобы при необходимости можно было прокручивать весь список новостей, а не только набор событий высокой важности. Вот как мы это будем делать:
- Динамический дизайн полосы прокрутки — реализуем скроллбар с иконками, меняющими цвет с черного (кликабельное состояние) на светло-серый (некликабельное состояние). Так мы будем иметь мгновенную визуальную обратную связь для навигации по большим спискам событий.
- Надежное хранение событий — разработаем систему хранения всех отфильтрованных событий, чтобы благодаря скроллу можно было найти любую новость, тем самым уберем лимит на отображение для более полной картины.
- Эффективный механизм обновления — мы также оптимизируем панель так, чтобы она перерисовывалась только при изменении фильтров, появлении новых событий или прокрутке.
- Улучшение пользовательского интерфейса — доработаем интерфейс с точной настройкой макета для интеграции скроллбара и области отображения событий для удобного дизайна.
Итак, это наш план для создания для панели, позволяющей легко просматривать экономические новости. Главный элемент этого улучшения — динамический скроллбар. Ниже представлена визуализация этого в общих чертах.

Реализация средствами MQL5
Для реализации улучшений в MQL5 прежде всего необходимо определить дополнительные объекты для скроллбара с помощью директивы #define вместе с нужными константами, переменные для отслеживания состояния прокрутки и обрабатываемых событий, а также массивы для хранения данных событий.
// Scrollbar UI elements #define SCROLL_UP_REC "SCROLL_UP_REC" #define SCROLL_UP_LABEL "SCROLL_UP_LABEL" #define SCROLL_DOWN_REC "SCROLL_DOWN_REC" #define SCROLL_DOWN_LABEL "SCROLL_DOWN_LABEL" #define SCROLL_LEADER "SCROLL_LEADER" #define SCROLL_SLIDER "SCROLL_SLIDER" //---- Scrollbar layout constants #define LIST_X 62 #define LIST_Y 162 #define LIST_WIDTH 716 #define LIST_HEIGHT 286 #define VISIBLE_ITEMS 11 #define ITEM_HEIGHT 26 #define SCROLLBAR_X (LIST_X + LIST_WIDTH + 2) // 780 #define SCROLLBAR_Y LIST_Y #define SCROLLBAR_WIDTH 20 #define SCROLLBAR_HEIGHT LIST_HEIGHT // 286 #define BUTTON_SIZE 15 #define BUTTON_WIDTH (SCROLLBAR_WIDTH - 2) #define BUTTON_OFFSET_X 1 #define SCROLL_AREA_HEIGHT (SCROLLBAR_HEIGHT - 2 * BUTTON_SIZE) #define SLIDER_MIN_HEIGHT 20 #define SLIDER_WIDTH 18 #define SLIDER_OFFSET_X 1 //---- Event name tracking string current_eventNames_data[]; string previous_eventNames_data[]; string last_dashboard_eventNames[]; string previous_displayable_eventNames[]; string current_displayable_eventNames[]; datetime last_dashboard_update = 0; //---- Filter flags bool enableCurrencyFilter = true; bool enableImportanceFilter = true; bool enableTimeFilter = true; bool isDashboardUpdate = true; bool filters_changed = true; //---- Scrollbar flags and variables bool scroll_visible = false; bool moving_state_slider = false; int scroll_pos = 0; int prev_scroll_pos = -1; // Track previous scroll position int mlb_down_x = 0; int mlb_down_y = 0; int mlb_down_yd_slider = 0; int prev_mouse_state = 0; int slider_height = SLIDER_MIN_HEIGHT; //---- Event counters int totalEvents_Considered = 0; int totalEvents_Filtered = 0; int totalEvents_Displayable = 0; //---- Global arrays for events EconomicEvent allEvents[]; EconomicEvent filteredEvents[]; EconomicEvent displayableEvents[];
Это закладывает основу для динамического скроллбара и улучшенного отображения событий. Мы определяем компоненты полосы прокрутки с помощью констант SCROLL_UP_LABEL, SCROLL_DOWN_LABEL, SCROLL_UP_REC и SCROLL_DOWN_REC, которые идентифицируют графические элементы кнопок вверх и вниз.
Константы для размещения LIST_X (62), LIST_Y (162), LIST_WIDTH (716) и LIST_HEIGHT (286), задают область отображения событий, а SCROLLBAR_X (780), SCROLLBAR_Y (162), SCROLLBAR_WIDTH (20) и SCROLLBAR_HEIGHT (286) — позицию полосы прокрутки. Параметры VISIBLE_ITEMS (11) и ITEM_HEIGHT (26) обеспечивают отображение 11 событий высотой по 26 пикселей каждое, BUTTON_SIZE (15) и SLIDER_WIDTH (18) формируют кнопки и ползунок.
Для управления событиями и взаимодействиями объявляем массивы current_displayable_eventNames и previous_displayable_eventNames для отслеживания названий событий и выявления изменений — это нужно для "тихого" обновления, а также last_dashboard_update для фиксации собственно времени обновления панели. Флаги фильтрации enableCurrencyFilter, enableImportanceFilter и enableTimeFilter (все со значением true) управляют отбором событий, а isDashboardUpdate и filters_changed определяют моменты обновления. Переменные полосы прокрутки scroll_visible, scroll_pos, prev_scroll_pos и moving_state_slider отслеживают видимость и позицию, а mlb_down_x, mlb_down_y и slider_height нужны, чтобы обеспечить возможность перетаскивать ползунок.
Мы используем счетчики totalEvents_Considered, totalEvents_Filtered и totalEvents_Displayable для мониторинга обработки событий и массивы allEvents, filteredEvents и displayableEvents для хранения данных событий. Это нужно для навигации по всем отфильтрованным новостям. После этого сначала создадим элементы скроллбара, а затем скорректируем размеры и позиции основного прямоугольника для размещения полосы прокрутки справа.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Enable mouse move events for scrollbar ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); // Create dashboard UI createRecLabel(MAIN_REC,50,50,740+13,410,clrSeaGreen,1); createRecLabel(SUB_REC1,50+3,50+30,740-3-3+13,410-30-3,clrWhite,1); createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10+5,clrGreen,1); createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15); //--- }
В обработчике события OnInit мы используем функцию ChartSetInteger для активации события перемещения мыши (mouse move), устанавливая его в значение true, чтобы управлять приоритетом прокрутки основного графика и наших пользовательских объектов. Это позволит прокручивать вертикальную полосу прокрутки и ее элементы, обеспечивая плавное движение и переходы. Затем мы увеличиваем ширину основного прямоугольника и подпрямоугольника 1 на 13 пикселей, чтобы разместить вертикальную полосу прокрутки. Мы также увеличиваем высоту подпрямоугольника 2 на 5 пикселей, чтобы разместить все 11 событий, устраняя эффект переполнения. После компиляции получаем следующий результат.

На изображении показаны все изменения, пронумерованные от 1 до 3. Изменение 1 показывает смещение кнопки отмены к краю панели, изменение 2 — корректировку ширины основных прямоугольников для размещения кнопки отмены и полосы прокрутки, а 3 — изменение высоты прямоугольника панели, чтобы не было переполнения последней строки элементов. Далее перейдем к определению и созданию скроллбара в выделенном пространстве. Однако, поскольку нам нужен динамический ползунок, который нужно отображать только при необходимости, нам потребуется определить функции для реализации этой логики.
//+------------------------------------------------------------------+ //| Calculate slider height | //+------------------------------------------------------------------+ int calculateSliderHeight() { if (totalEvents_Filtered <= VISIBLE_ITEMS) return SCROLL_AREA_HEIGHT; double visible_ratio = (double)VISIBLE_ITEMS / totalEvents_Filtered; int height = (int)::floor(SCROLL_AREA_HEIGHT * visible_ratio); return MathMax(SLIDER_MIN_HEIGHT, MathMin(height, SCROLL_AREA_HEIGHT)); } //+------------------------------------------------------------------+ //| Update slider position | //+------------------------------------------------------------------+ void updateSliderPosition() { int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS); if (max_scroll <= 0) return; double scroll_ratio = (double)scroll_pos / max_scroll; int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE; int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height; int new_y = scroll_area_y_min + (int)(scroll_ratio * (scroll_area_y_max - scroll_area_y_min)); ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); if (debugLogging) Print("Slider moved to y=", new_y); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Update button colors based on scroll position | //+------------------------------------------------------------------+ void updateButtonColors() { int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS); if (scroll_pos == 0) { ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrLightGray); } else { ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrBlack); } if (scroll_pos >= max_scroll) { ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrLightGray); } else { ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrBlack); } ChartRedraw(0); }
Здесь мы реализуем три основные функции — calculateSliderHeight, updateSliderPosition и updateButtonColors. Они дадут нам интуитивную навигацию и четкой визуальную обратную связь. В функции calculateSliderHeight мы определяем высоту ползунка полосы прокрутки, чтобы визуально отразить соотношение видимых событий к общему числу отфильтрованных событий.
Если значение totalEvents_Filtered меньше либо равно VISIBLE_ITEMS (11), возвращается SCROLL_AREA_HEIGHT (256 пикселей), заполняя всю область прокрутки, если все события помещаются на одном экране. В противном случае вычисляется отображаемая часть visible_ratio как отношение VISIBLE_ITEMS к totalEvents_Filtered, умножается на SCROLL_AREA_HEIGHT, и с помощью функции floor мы получаем целое значение величины height. Затем возвращается максимум из SLIDER_MIN_HEIGHT (20 пикселей) и минимум между значений height и SCROLL_AREA_HEIGHT. Это даст корректный размер ползунка относительно масштаба списка событий.
В функции «pdateSliderPosition мы позиционируем ползунок в соответствии с текущей позицией прокрутки списка событий. Для этого вычисляем значение max_scroll как разницу между ArraySize(displayableEvents) и VISIBLE_ITEMS, используя функцию MathMax, чтобы исключить отрицательные значения. Если max_scroll равен нулю (прокрутка не требуется), Выходим из функции. Далее вычисляем коэффициент скролла scroll_ratio как отношение scroll_pos к max_scroll, задаем вертикальный диапазон движения ползунка с помощью scroll_area_y_min (SCROLLBAR_Y + BUTTON_SIZE) и scroll_area_y_max (scroll_area_y_min» + SCROLL_AREA_HEIGHT - slider_height). После чего рассчитывается новая позиция new_y через интерполяцию scroll_ratio в пределах этого диапазона.
Затем с помощью функции ObjectSetInteger мы устанавливаем свойство OBJPROP_YDISTANCE объекта SCROLL_SLIDER в значение new_y, и если debugLogging равно true, записываем это перемещение в журнал. Также вызывается ChartRedraw для обновления отображения.
В функции updateButtonColors мы динамически изменяем цвета иконок кнопок вверх и вниз, показывая тем самым их кликабельность для лучшей обратной связи с пользователем. Мы вычисляем максимальное значение скролла max_scroll аналогично функции updateSliderPosition и проверяем значение scroll_pos, чтобы определить состояния SCROLL_UP_LABEL и SCROLL_DOWN_LABEL. Если значение позиции scroll_pos равно 0, устанавливаем для объекта SCROLL_UP_LABEL свойство OBJPROP_COLOR в значение clrLightGray (некликабельное состояние, верх списка). В противном случае — clrBlack (кликабельное состояние). Также, если значение scroll_pos равно или превышает значение максимума max_scroll, для параметра SCROLL_DOWN_LABEL устанавливается значение цвета clrLightGray (некликабельное состояние, низ списка). Если нет — цвет clrBlack (кликабельное состояние).
В завершение вызываем метод ChartRedraw для перерисовки графика. Теперь мы можем динамически создавать скроллбар при обновлении значений панели, как показано ниже.
// Update TIME_LABEL string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static"; updateLabel(TIME_LABEL,timeText+" ||| Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered)); // Update scrollbar visibility bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS; if (new_scroll_visible != scroll_visible || events_changed || filters_changed) { scroll_visible = new_scroll_visible; if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden"); if (scroll_visible) { if (ObjectFind(0, SCROLL_LEADER) < 0) { createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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(); } else { 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); if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered); } }
Для удобства мы в интерфейсе реализуем ключевые обновления отображения времени на панели и динамического скроллбара для удобной работы и навигации по новостям. Значение времени TIME_LABEL обновляется и отображает текущее серверное время и статистику событий. Мы полностью контролируем видимость скроллбара и инициализируем ее компоненты с динамическими цветами иконок. Так мы получаем интуитивную систему навигации.
Сначала обновляем значение метки TIME_LABEL, чтобы отображать текущее время и статус обработки событий. Также создаем строковое значение timeText с использованием условия: если значение updateServerTime равно true, вызывается функция TimeToString со значением TimeCurrent и флагами TIME_DATE|TIME_SECONDS для форматирования серверного времени; в противном случае устанавливается значение "Server Time: Static". Затем с помощью функции updateLabel задаем для TIME_LABEL текст "timeText", объединенный с разделителем (" ||| ") и счетчиками событий, преобразованными через IntegerToString для "totalEvents_Filtered" и "totalEvents_Considered". В результате получаем отображение вида "Server Time: 2025.03.01 12:00:00 ||| Total News: 1711/3000", где четко показано соотношение отфильтрованных и общего количества событий, при этом мы исключаем отображение только выводимых новостей, как это было в предыдущих версиях.
Далее реализуем логику видимости скроллбара и создание нужных компонентов, чтобы он отображался только при необходимости и обеспечивал визуальную обратную связь. Мы определяем необходимость отображения new_scroll_visible - для этого проверяем, превышает ли общее количество новостей totalEvents_Filtered значение VISIBLE_ITEMS (у нас это 11). Это указывает на превышение числа отображаемых одновременно событий. Если значение "new_scroll_visible" отличается от "scroll_visible", либо если "events_changed" или "filters_changed" равны true, обновляем значение "scroll_visible" и выводим в журнал состояние через Print (если включено логирование debugLogging). Если значение scroll_visible равно true, с помощью ObjectFind проверяем наличие объекта SCROLL_LEADER и, если его нет, создаем скроллбар. Для этого вызываем функцию createRecLabel для создания объектов SCROLL_LEADER, SCROLL_UP_REC и SCROLL_DOWN_REC в позициях, заданных параметрами SCROLLBAR_X, SCROLLBAR_Y, BUTTON_OFFSET_X и BUTTON_SIZE. Используем для них цвета clrSilver и clrDarkGray.
Вычисляем значение максимума max_scroll с помощью функции MathMax и ArraySize(displayableEvents) минус VISIBLE_ITEMS, определяем цвета up_color и down_color равными clrLightGray или clrBlack в зависимости от значений scroll_pos и max_scroll, и с помощью createLabel создаем метки SCROLL_UP_LABEL и SCROLL_DOWN_LABEL, используя CharToString для стрелок шрифта Webdings, а именно 5 и 6, как показано ниже.

Если вам интересно, почему мы использовали 0x35 вместо просто 5, это шестнадцатеричный формат (base-16). Того же результата можно добиться, если использовать 5 как строковое значение. Если вы хотите использовать ASCII-код символа, можно напрямую преобразовать 53 в строковое значение с помощью функции CharToString(53) и получить тот же результат. Существует множество вариантов, поэтому выбор за вами. Ниже приведена информация по коду.

Далее вычисляем высоту ползунка slider_height с помощью calculateSliderHeight, создаем объект SCROLL_SLIDER с помощью функции createButton в позиции slider_y, устанавливаем его свойство OBJPROP_WIDTH равным 2. Выводим детали в журнал, если включено debugLogging. Затем вызываем функции "updateSliderPosition" и "updateButtonColors" для инициализации позиции ползунка и цветов иконок. Если значение параметра scroll_visible равно false, удаляем объекты полосы прокрутки с помощью функции ObjectDelete для переменных SCROLL_LEADER, SCROLL_UP_REC, SCROLL_DOWN_REC, SCROLL_UP_LABEL, SCROLL_DOWN_LABEL и SCROLL_SLIDER. Выводим данные об удалении в журнал для поддержания чистоты интерфейса, когда прокрутка не требуется. Остальная часть функции, содержащая данную логику, а также функция, отвечающая за хранение событий, приведены ниже.
//+------------------------------------------------------------------+ //| Update dashboard values | //+------------------------------------------------------------------+ void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) { totalEvents_Considered = 0; totalEvents_Filtered = 0; totalEvents_Displayable = 0; ArrayFree(current_eventNames_data); ArrayFree(current_displayable_eventNames); datetime timeRange = PeriodSeconds(range_time); datetime timeBefore = TimeTradeServer() - timeRange; datetime timeAfter = TimeTradeServer() + timeRange; // Populate displayableEvents if (MQLInfoInteger(MQL_TESTER)) { if (filters_changed) { FilterEventsForTester(); ArrayFree(displayableEvents); // Clear displayableEvents on filter change } int eventIndex = 0; for (int i = 0; i < ArraySize(filteredEvents); i++) { totalEvents_Considered++; datetime eventDateTime = filteredEvents[i].eventDateTime; if (eventDateTime < StartDate || eventDateTime > EndDate) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); continue; } bool timeMatch = !enableTimeFilter; if (enableTimeFilter) { if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true; else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true; } if (!timeMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter."); continue; } bool currencyMatch = !enableCurrencyFilter; if (enableCurrencyFilter) { for (int j = 0; j < ArraySize(curr_filter_array); j++) { if (filteredEvents[i].currency == curr_filter_array[j]) { currencyMatch = true; break; } } } if (!currencyMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); continue; } bool importanceMatch = !enableImportanceFilter; if (enableImportanceFilter) { string imp_str = filteredEvents[i].importance; ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE : (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW : (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE : CALENDAR_IMPORTANCE_HIGH; for (int k = 0; k < ArraySize(imp_filter_array); k++) { if (event_imp == imp_filter_array[k]) { importanceMatch = true; break; } } } if (!importanceMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter."); continue; } ArrayResize(displayableEvents, eventIndex + 1); displayableEvents[eventIndex] = filteredEvents[i]; ArrayResize(current_displayable_eventNames, eventIndex + 1); current_displayable_eventNames[eventIndex] = filteredEvents[i].event; eventIndex++; } totalEvents_Filtered = ArraySize(displayableEvents); if (debugLogging) Print("Tester mode: Stored ", totalEvents_Filtered, " displayable events."); } else { MqlCalendarValue values[]; datetime startTime = TimeTradeServer() - PeriodSeconds(start_time); datetime endTime = TimeTradeServer() + PeriodSeconds(end_time); int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL); int eventIndex = 0; if (filters_changed) ArrayFree(displayableEvents); // Clear displayableEvents on filter change for (int i = 0; i < allValues; i++) { MqlCalendarEvent event; CalendarEventById(values[i].event_id, event); MqlCalendarCountry country; CalendarCountryById(event.country_id, country); MqlCalendarValue value; CalendarValueById(values[i].id, value); totalEvents_Considered++; bool currencyMatch = false; if (enableCurrencyFilter) { for (int j = 0; j < ArraySize(curr_filter_array); j++) { if (country.currency == curr_filter_array[j]) { currencyMatch = true; break; } } if (!currencyMatch) continue; } bool importanceMatch = false; if (enableImportanceFilter) { for (int k = 0; k < ArraySize(imp_filter_array); k++) { if (event.importance == imp_filter_array[k]) { importanceMatch = true; break; } } if (!importanceMatch) continue; } bool timeMatch = false; if (enableTimeFilter) { datetime eventTime = values[i].time; if (eventTime <= TimeTradeServer() && eventTime >= timeBefore) timeMatch = true; else if (eventTime >= TimeTradeServer() && eventTime <= timeAfter) timeMatch = true; if (!timeMatch) continue; } ArrayResize(displayableEvents, eventIndex + 1); displayableEvents[eventIndex].eventDate = TimeToString(values[i].time,TIME_DATE); displayableEvents[eventIndex].eventTime = TimeToString(values[i].time,TIME_MINUTES); displayableEvents[eventIndex].currency = country.currency; displayableEvents[eventIndex].event = event.name; displayableEvents[eventIndex].importance = (event.importance == CALENDAR_IMPORTANCE_NONE) ? "None" : (event.importance == CALENDAR_IMPORTANCE_LOW) ? "Low" : (event.importance == CALENDAR_IMPORTANCE_MODERATE) ? "Medium" : "High"; displayableEvents[eventIndex].actual = value.GetActualValue(); displayableEvents[eventIndex].forecast = value.GetForecastValue(); displayableEvents[eventIndex].previous = value.GetPreviousValue(); displayableEvents[eventIndex].eventDateTime = values[i].time; ArrayResize(current_displayable_eventNames, eventIndex + 1); current_displayable_eventNames[eventIndex] = event.name; eventIndex++; } totalEvents_Filtered = ArraySize(displayableEvents); if (debugLogging) Print("Live mode: Stored ", totalEvents_Filtered, " displayable events."); } // Check for changes in displayable events bool events_changed = isChangeInStringArrays(previous_displayable_eventNames, current_displayable_eventNames); bool scroll_changed = (scroll_pos != prev_scroll_pos); if (events_changed || filters_changed || scroll_changed) { if (debugLogging) { if (events_changed) Print("Changes detected in displayable events."); if (filters_changed) Print("Filter changes detected."); if (scroll_changed) Print("Scroll position changed: ", prev_scroll_pos, " -> ", scroll_pos); } ArrayFree(previous_displayable_eventNames); ArrayCopy(previous_displayable_eventNames, current_displayable_eventNames); prev_scroll_pos = scroll_pos; // Clear and redraw UI ObjectsDeleteAll(0, DATA_HOLDERS); ObjectsDeleteAll(0, ARRAY_NEWS); int startY = LIST_Y; int start_idx = scroll_visible ? scroll_pos : 0; int end_idx = MathMin(start_idx + VISIBLE_ITEMS, ArraySize(displayableEvents)); for (int i = start_idx; i < end_idx; i++) { totalEvents_Displayable++; color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite; createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),LIST_X,startY-1,LIST_WIDTH,ITEM_HEIGHT+1,holder_color,1,clrNONE); int startX = LIST_X + 3; string news_data[ArraySize(array_calendar)]; news_data[0] = displayableEvents[i].eventDate; news_data[1] = displayableEvents[i].eventTime; news_data[2] = displayableEvents[i].currency; color importance_color = clrBlack; if (displayableEvents[i].importance == "Low") importance_color = clrYellow; else if (displayableEvents[i].importance == "Medium") importance_color = clrOrange; else if (displayableEvents[i].importance == "High") importance_color = clrRed; news_data[3] = ShortToString(0x25CF); news_data[4] = displayableEvents[i].event; news_data[5] = DoubleToString(displayableEvents[i].actual, 3); news_data[6] = DoubleToString(displayableEvents[i].forecast, 3); news_data[7] = DoubleToString(displayableEvents[i].previous, 3); for (int k = 0; k < ArraySize(array_calendar); k++) { if (k == 3) { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri"); } else { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri"); } startX += buttons[k]+3; } ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1); current_eventNames_data[ArraySize(current_eventNames_data)-1] = displayableEvents[i].event; startY += ITEM_HEIGHT; } if (debugLogging) Print("Displayed ", totalEvents_Displayable, " events, start_idx=", start_idx, ", end_idx=", end_idx); } else { if (debugLogging) Print("No changes detected. Skipping redraw."); } // Update TIME_LABEL string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static"; updateLabel(TIME_LABEL,timeText+" ||| Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered)); // Update scrollbar visibility bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS; if (new_scroll_visible != scroll_visible || events_changed || filters_changed) { scroll_visible = new_scroll_visible; if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden"); if (scroll_visible) { if (ObjectFind(0, SCROLL_LEADER) < 0) { createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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(); } else { 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); if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered); } } if (isChangeInStringArrays(previous_eventNames_data, current_eventNames_data)) { if (debugLogging) Print("CHANGES IN EVENT NAMES DETECTED."); ArrayFree(previous_eventNames_data); ArrayCopy(previous_eventNames_data, current_eventNames_data); } }
Здесь в функции update_dashboard_values мы дорабатываем панель. Добавляем в нее новую логику для улучшенного отображения событий и динамической функциональности скроллбара, также приоритет отдаем "тихим" обновлениям для повышения эффективности. Используем массив displayableEvents для хранения всех отфильтрованных событий, реализуем механизм отслеживания изменений, чтобы избежать ненужных перерисовок, и интегрируем скроллбар для интуитивной и удобной навигации. Для начала сбросим счетчики totalEvents_Considered, totalEvents_Filtered и totalEvents_Displayable, а также очищаем массивы current_eventNames_data и current_displayable_eventNames с помощью функции ArrayFree для подготовки к обновленным новостным данным.
Мы сохраняем существующую логику фильтрации для режимов тестирования и реальной торговли и вводим массив displayableEvents для хранения всех отфильтрованных событий, обеспечивая полный доступ (например, к 1711 событиям), в отличие от исходной проблемы, когда отображались только 5–6 событий. В режиме тестера мы вызываем FilterEventsForTester, и если filters_changed равно true, очищаем displayableEvents через ArrayFree и проходим циклом по filteredEvents, используем фильтры (enableTimeFilter, enableCurrencyFilter, enableImportanceFilter) для заполнения displayableEvents и current_displayable_eventNames, устанавливая параметр totalEvents_Filtered равным ArraySize(displayableEvents).
В режиме реальной торговли мы используем CalendarValueHistory для получения событий, очищаем параметр displayableEvents, если filters_changed равно true, и аналогично сохраняем отфильтрованные события, выводя их количество в журнал (если включено debugLogging).
Мы реализуем тихие обновления. Для этого используем функцию isChangeInStringArrays и сравниваем previous_displayable_eventNames с current_displayable_eventNames - так определяем количество events_changed. Также проверяем разнице между scroll_pos и prev_scroll_pos для определения количества scroll_changed. Если значение events_changed, filters_changed или scroll_changed равно true, записываем изменения в лог через Print (при включенном debugLogging), обновляем previous_displayable_eventNames с помощью ArrayCopy и устанавливаем позицию prev_scroll_pos. Очищаем элементы интерфейса с помощью метода ObjectsDeleteAll для DATA_HOLDERS и ARRAY_NEWS, затем отображаем до VISIBLE_ITEMS (11) событий из массива displayableEvents, начиная с индекса start_idx (определяется scroll_pos, если scroll_visible равно true) до end_idx (ограничивается ArraySize(displayableEvents)).
Для каждого события мы вызываем параметр createRecLabel для создания фонового прямоугольника с чередующимися цветами (C'213,227,207' или clrWhite), заполняем news_data деталями события и используем createLabel для отображения полей. Устанавливаем нужный цвет важности importance_color (например, clrYellow для Low), а также выводим количество отображенных событий при включенном логировании debugLogging. Если изменений не произошло, мы пропускаем перерисовку и фиксируем это в журнале.
Мы интегрируем скроллбар. Для этого устанавливаем new_scroll_visible, если totalEvents_Filtered превышает VISIBLE_ITEMS, обновляем "scroll_visible" при изменении значения или если events_changed либо filters_changed равно true, и создаем компоненты (SCROLL_LEADER, SCROLL_UP_REC, SCROLL_UP_LABEL, SCROLL_DOWN_REC, SCROLL_DOWN_LABEL, SCROLL_SLIDER) с помощью createRecLabel, createLabel и createButton, используя цвет clrBlack или clrLightGray для иконок в зависимости от параметров scroll_pos и max_scroll. Когда компоненты не нужны, удаляем их с помощью ObjectDelete и обновляем previous_eventNames_data, если функция isChangeInStringArrays обнаруживает изменения в current_eventNames_data. Поскольку мы создали новые объекты, их необходимо удалять вместе с основной панелью.
//+------------------------------------------------------------------+ //| Destroy dashboard | //+------------------------------------------------------------------+ void destroy_Dashboard() { ObjectDelete(0,"MAIN_REC"); ObjectDelete(0,"SUB_REC1"); ObjectDelete(0,"SUB_REC2"); ObjectDelete(0,"HEADER_LABEL"); ObjectDelete(0,"TIME_LABEL"); ObjectDelete(0,"IMPACT_LABEL"); ObjectsDeleteAll(0,"ARRAY_CALENDAR"); ObjectsDeleteAll(0,"ARRAY_NEWS"); ObjectsDeleteAll(0,"DATA_HOLDERS"); ObjectsDeleteAll(0,"IMPACT_LABEL"); ObjectDelete(0,"FILTER_LABEL"); ObjectDelete(0,"FILTER_CURR_BTN"); ObjectDelete(0,"FILTER_IMP_BTN"); ObjectDelete(0,"FILTER_TIME_BTN"); ObjectDelete(0,"CANCEL_BTN"); ObjectsDeleteAll(0,"CURRENCY_BTNS"); 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); ArrayFree(displayableEvents); ArrayFree(current_displayable_eventNames); ArrayFree(previous_displayable_eventNames); ChartRedraw(0); }
Для удаления созданных объектов мы просто вызываем функцию ObjectDelete, передавая соответствующие имена, чтобы гарантировать их удаление при удалении панели, поскольку теперь они являются ее частью. После компиляции получаем следующий результат.
Менее или равно 11 отфильтрованных событий.

Более 11 отфильтрованных событий.

Широкий диапазон отфильтрованных событий.

На рисунках видно, что скроллбар календаря создается динамически в зависимости от количества событий. Теперь нужно "оживить" элементы скроллбара. Это сделаем в функции 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; if (id == CHARTEVENT_OBJECT_CLICK) { // 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); } } else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { 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); 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 = SCROLLBAR_Y + 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; } }
Здесь мы улучшаем интерактивность, реализуя новую логику для скроллбара в функции OnChartEvent. Это позволит плавно перемещаться по событиям посредством кликов и перетаскивания. Нам нужна обработка взаимодействия пользователя со скроллбаром, в частности - обработка кликов по кнопкам вверх и вниз, а также перемещений мыши для перетаскивания ползунка. Нужно обеспечить быстрое обновление отображения панели. Для этого обрабатываем события CHARTEVENT_OBJECT_CLICK для кликов по кнопкам crhjkk,fhf, когда значение параметра scroll_visible равно true. Если объект, по которому кликнули, (sparam) — это SCROLL_UP_REC или SCROLL_UP_LABEL, вызываем функцию scrollUp для уменьшения scroll_pos, затем вызываем updateButtonColors для обновления цветов иконок (clrBlack или clrLightGray) в зависимости от новой позиции. Записываем действие в журнал с помощью Print, если включено debugLogging, и вызываем ChartRedraw для обновления отображения.
Аналогично, при клике на SCROLL_DOWN_REC или SCROLL_DOWN_LABEL вызываем scrollDown для увеличения положения scroll_pos, затем функцию обновления цветов updateButtonColors, логируем событие и обновляем график через функцию ChartRedraw, обеспечивая корректное отображение прокрученного списка событий. Функции уже определены, их логику мы объясним далее.
Для событий перетаскивания мыши CHARTEVENT_MOUSE_MOVE при scroll_visible равном true мы управляем перетаскиванием ползунка. Когда значение предыдущего состояния prev_mouse_state равно 0, а текущего mouse_state равно 1, используем ObjectGetInteger для получения позиции ползунка SCROLL_SLIDER (OBJPROP_XDISTANCE, OBJPROP_YDISTANCE) и его размеров (OBJPROP_XSIZE, OBJPROP_YSIZE) в переменные xd, yd, xs и ys. Если координаты мыши (mouse_x, mouse_y) находятся в пределах ползунка, мы устанавливаем значение параметра moving_state_slider равным true, сохраняем mlb_down_x, mlb_down_y и mlb_down_yd_slider, изменяем цвет OBJPROP_BGCOLOR объекта ползунка SCROLL_SLIDER на clrDodgerBlue и увеличиваем размер OBJPROP_YSIZE на 2, отключаем прокрутку графика через ChartSetInteger и логируем начало перетаскивания при включенном логировании debugLogging.
Пока значения параметров moving_state_slider и mouse_state равны true, мы вычисляем значение delta_y как разницу между mouse_y и mlb_down_y, рассчитываем новое значение new_y в пределах scroll_area_y_min (рассчитывается как SCROLLBAR_Y + BUTTON_SIZE) и scroll_area_y_max (рассчитывается как scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height) с помощью функций MathMax и MathMin и устанавливаем значение OBJPROP_YDISTANCE объекта SCROLL_SLIDER равным новому new_y. Мы определяем новое положение ползунка new_scroll_pos на основе параметра scroll_ratio нового положения new_y в пределах диапазона прокрутки, и если он отличается от scroll_pos, обновляем scroll_pos, вызываем функцию update_dashboard_values с curr_filter_selected и imp_filter_selected, затем функцию обновления цветов updateButtonColors, логируем детали перетаскивания и вызываем перерисовку графика ChartRedraw.
Когда параметр mouse_state становится равным 0, сбрасываем значение moving_state_slider, возвращаем цвет OBJPROP_BGCOLOR объекта ползунка SCROLL_SLIDER обратно в цвет clrLightSlateGray, а OBJPROP_YSIZE в значение slider_height, повторно включаем прокрутку графика, логируем завершение перетаскивания и вызываем ChartRedraw для перерисовки. Так получается плавное взаимодействие с ползунком. Ниже приведены функции, отвечающие за логику прокрутки.
//+------------------------------------------------------------------+ //| Scroll up | //+------------------------------------------------------------------+ void scrollUp() { if (scroll_pos > 0) { scroll_pos--; update_dashboard_values(curr_filter_selected, imp_filter_selected); updateSliderPosition(); if (debugLogging) Print("Scrolled up. CurrPos: ", scroll_pos); } else { if (debugLogging) Print("Cannot scroll up further. Already at top."); } } //+------------------------------------------------------------------+ //| Scroll down | //+------------------------------------------------------------------+ void scrollDown() { int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS); if (scroll_pos < max_scroll) { scroll_pos++; update_dashboard_values(curr_filter_selected, imp_filter_selected); updateSliderPosition(); if (debugLogging) Print("Scrolled down. CurrPos: ", scroll_pos); } else { if (debugLogging) Print("Cannot scroll down further. Max scroll reached: ", max_scroll); } } //+------------------------------------------------------------------+
В функции scrollUp мы реализуем навигацию вверх по списку событий. Проверяем, является ли значение scroll_pos больше нуля — это будет означать возможность прокрутки вверх. Если это так, уменьшаем значение scroll_pos, вызываем функцию update_dashboard_values с параметрами curr_filter_selected и imp_filter_selected для обновления отображаемых событий, затем updateSliderPosition для корректировки позиции ползунка SCROLL_SLIDER и выводим в журнал новое значение положения scroll_pos с помощью функции Print (при включенном журналировании debugLogging). Если положение scroll_pos равно 0, мы выводим сообщение о том, что достигнута верхняя граница списка — этот предотвратит лишние обновления.
В функции scrollDown реализуем навигацию вниз по списку событий. Вычисляем значение max_scroll с помощью функции MathMax (значение должно быть неотрицательным) как разницу между ArraySize(displayableEvents) и VISIBLE_ITEMS (11). Это будет максимально возможная позиция прокрутки.
Если значение параметра scroll_pos меньше max_scroll, увеличиваем scroll_pos, вызываем функцию update_dashboard_values с параметрами curr_filter_selected и imp_filter_selected для обновления отображаемых событий, затем функцию updateSliderPosition для перемещения ползунка SCROLL_SLIDER и записываем в журнал новое значение scroll_pos с помощью функции Print (при активном debugLogging). Если значение scroll_pos равно или больше max_scroll, выводим сообщение о достижении нижней границы списка, также чтобы не было избыточных обновлений отображения. Чтобы взаимодействие было гладким, вызываем определенные функции как в обработчике OnInit, так и в обработчике OnTick.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { UpdateFilterInfo(); CheckForNewsTrade(); if (isDashboardUpdate) { if (MQLInfoInteger(MQL_TESTER)) { datetime currentTime = TimeTradeServer(); datetime timeRange = PeriodSeconds(range_time); datetime timeAfter = currentTime + timeRange; if (filters_changed || last_dashboard_update < timeAfter) { update_dashboard_values(curr_filter_selected, imp_filter_selected); ArrayFree(last_dashboard_eventNames); ArrayCopy(last_dashboard_eventNames, current_eventNames_data); last_dashboard_update = currentTime; } } else { update_dashboard_values(curr_filter_selected, imp_filter_selected); } } } //+------------------------------------------------------------------+ //| 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; 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); 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); 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); 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); 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); imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl; ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1); 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); 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); } } else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { 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); 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 = SCROLLBAR_Y + 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; } }
Здесь мы просто вызываем функции и реализованную логику в соответствующих обработчиках событий, чтобы изменения вступали в силу при любом необходимом событии. После компиляции мы получаем следующий результат.

Из этой анимации видно, что в панель добавлена динамическая полоса прокрутки. Теперь остается тщательно протестировать систему. Это мы сделаем в следующем разделе.
Тестирование и проверка
Мы протестировали изменения в панели, чтобы проверить корректную работу динамического скроллбара и отображения событий, обеспечивающих плавную навигацию по новостям. При тестировании проверяли визуальную обратную связь полосы прокрутки, отображение всех отфильтрованных событий и эффективность "тихих" обновлений как в режиме реальной торговли, так и в тестере стратегий. Результаты тестирования показаны в компактном формате GIF (Graphics Interchange Format), чтобы наглядно продемонстрировать работу панели.

На анимации видно, что скроллбар работает корректно, однако возникает проблема: при изменении фильтров кликом по ним скроллбар не обновляется динамически, хотя события меняются правильно. Это потому что не хватает пересчета, из-за чего функциональность не полностью динамическая. Чтобы устранить это, необходимо перекалибровывать скроллбар каждый раз при нажатии на кнопки. То же самое можно добиться, если просто обновлять скроллбар при изменении данных, но это снова привело бы к избыточным процессам без особой необходимости. Ниже приведена полная логика для решения этой проблемы.
//+------------------------------------------------------------------+ //| 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; 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, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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); imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl; ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1); 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, SCROLLBAR_X, SCROLLBAR_Y, 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, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings"); int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings"); slider_height = calculateSliderHeight(); int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + 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); } } else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { 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); 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 = SCROLLBAR_Y + 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; } }
Здесь мы просто взяли логику, реализованную для кликов по элементам скроллбара, и расширили ее на отдельные кнопки фильтров. Подробно останавливаться на этом, думаю, не требуется. После компиляции получаем следующий результат.

НА анимации видно, что теперь все работает корректно — с динамическими обновлениями и хорошим отображением событий.
Заключение
В заключение отметим, что мы развили еще немного нашу серию, посвященную Экономическому календарю MQL5, внедрив динамический скроллбар и улучшенное отображение событий, обеспечив интуитивную навигацию и удобный доступ к новостям, что показано в GIF-анимации. За основу улучшений мы брали разработки из Части 8, поэтому добавленный функционал работает как в режиме реальной торговли, так и в тестере, предоставляя надежную платформу для торговых стратегий по новостям. Вы можете использовать эту доработанную панель в качестве основы и адаптировать ее под свои торговые требования.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18135
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Пользовательские инструменты отладки и профилирования для разработки на MQL5 (Часть I): Расширенное логирование
Торговые инструменты на MQL5 (Часть 5): Создание бегущей тикерной строки для мониторинга символов в реальном времени
От начального до среднего уровня: Struct (VII)
Нейросети в трейдинге: Адаптивная факторная токенизация (Основные компоненты)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования