MQL5经济日历交易指南(第九部分):通过动态滚动条与界面优化提升新闻交互体验
概述
在本文中,我们通过为MQL5经济日历系列引入垂直动态滚动条和优化界面设计,显著提升了交易者与新闻事件的交互体验,确保导航直观并可靠获取事件信息。基于第八部分中优化的回测功能与智能筛选逻辑,我们进一步聚焦于打造响应式用户界面(UI):滚动条通过视觉反馈显示可点击状态,无论在实时还是回测环境中,都能快速高效地获取经济新闻。本文将围绕以下主题展开:
让我们即刻深入了解这些优化吧!
引入动态滚动条,轻松浏览新闻
为了提升我们与MQL5经济日历的交互体验,我们设想以一个动态滚动条作为直观浏览新闻的核心元素。我们计划设计一个响应式垂直滚动条,该滚动条通过视觉反馈显示可点击状态,同时搭配一个强大的事件存储系统,以确保能够无缝访问所有经过筛选的新闻,将仪表盘转变为一个流畅、以交易者为中心的工具。我们将取消对可显示事件数量的最小限制,并列出所有经过筛选的事件,以便在必要时滚动浏览所有新闻,从而消除仅能查看一组优先事件的限制。其实现方式如下:
- 动态滚动条设计: 我们将实现一个带有图标的滚动条,图标在可点击状态下显示为黑色,在不可点击状态下显示为浅灰色,从而提供即时视觉反馈,引导用户在大量事件列表中进行导航。
- 可靠的事件存储: 我们将开发一个系统来存储所有经过筛选的事件,确保每条新闻都能通过滚动条访问,消除显示限制,确保全面查看。
- 高效的更新机制:我们将优化仪表盘,使其仅在筛选条件改变、新事件出现或发生滚动时进行重绘,从而保持流畅且无干扰的体验。
- 美观的用户界面(UI)优化:我们将通过精确的布局调整来增强界面,确保滚动条和事件显示无缝集成,呈现出专业且用户友好的设计。
这一战略路线图将为构建一个仪表盘奠定基础,该仪表盘助力我们轻松探索经济新闻。动态滚动条将成为解锁精准交易体验的关键。简言之,以下是我们目标实现效果的示意图描述。

在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[];
在此,我们通过定义关键的UI常量、变量和数组,为MQL5经济日历的动态滚动条及精致的事件展示功能奠定基础,以实现直观的导航操作和高效的事件处理。我们使用诸如“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函数将鼠标移动事件初始化为启用状态(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”,增强MQL5经济日历动态滚动条的功能,以确保实现直观的导航操作和清晰的视觉反馈。在“calculateSliderHeight”函数中,我们根据可见事件数量与总筛选事件数量的比例,确定滚动条滑块的高度,以可视化方式呈现。
我们首先检查“totalEvents_Filtered”是否小于或等于“VISIBLE_ITEMS”(11),如果所有事件均可在一个视图中显示,则返回“SCROLL_AREA_HEIGHT”(256像素)以填满滚动区域。否则,我们通过将“VISIBLE_ITEMS”除以“totalEvents_Filtered”来计算“visible_ratio”,再将其乘以“SCROLL_AREA_HEIGHT”,并使用floor函数取整得到“height”。接下来,我们返回“SLIDER_MIN_HEIGHT”(20像素)与“height”和“SCROLL_AREA_HEIGHT”中的较小值中的较大值,以确保滑块尺寸与事件列表规模相匹配。
在“updateSliderPosition”函数中,我们根据事件列表中的当前滚动位置来定位滚动条的滑块。我们将“max_scroll”计算为“ArraySize(displayableEvents)”与“VISIBLE_ITEMS”的差值,并使用MathMax函数确保其非负,如果“max_scroll”为0(即无需滚动),则直接退出函数。我们通过将“scroll_pos”除以“max_scroll”来计算“scroll_ratio”,并定义滑块的垂直范围:其下限“scroll_area_y_min”为“SCROLLBAR_Y”加上“BUTTON_SIZE”,上限“scroll_area_y_max”为“scroll_area_y_min”加上“SCROLL_AREA_HEIGHT”再减去“slider_height”。最后,我们通过在该范围内根据“scroll_ratio”进行插值计算,得出滑块的新Y坐标“new_y”。
随后,我们使用ObjectSetInteger函数将“SCROLL_SLIDER”的“OBJPROP_YDISTANCE”)设置为“new_y”。如果“debugLogging”为true,则记录滑块移动日志,并调用ChartRedraw函数更新图表显示。
在“updateButtonColors”函数中,我们动态更新滚动条向上和向下按钮图标的颜色,以指示其可点击状态,从而增强用户反馈。我们按照“updateSliderPosition”函数中的方法计算“max_scroll”,并检查“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”,清晰地呈现筛选后事件数与总事件数的对比,彻底告别旧版“可显示新闻”的局限。
随后,我们实现滚动条的可见性逻辑及组件创建功能,确保其仅在需要时显示,并提供直观的视觉反馈。我们通过检查“totalEvents_Filtered”是否超过“VISIBLE_ITEMS”(11,即单页可显示事件数)来确定“new_scroll_visible”,如果超过,则表明需要滚动条以显示全部事件。如果“new_scroll_visible”与当前“scroll_visible”不同,或“events_changed”或“filters_changed”为true,则更新“scroll_visible”状态,并在“debugLogging”启用时,使用Print函数记录状态变更日志。当“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”。
我们使用MathMax函数计算“max_scroll”,其值为“ArraySize(displayableEvents)”减去“VISIBLE_ITEMS”,确保结果非负。根据“scroll_pos”与“max_scroll”的对比关系,将“up_color”和“down_color”分别设置为“clrLightGray”或“clrBlack”。随后,调用“createLabel”创建“SCROLL_UP_LABEL”和“SCROLL_DOWN_LABEL”,并使用CharToString绘制Web dings箭头,特别是如下所示的5和6:

如果您对于为何我们使用“0x35”而非直接使用数字5感到疑惑,这是因为“0x35”是十六进制(base-16)的二进制格式表示。实际上,如果您直接使用数字5(但需以字符串形式如"5"传入),也能达到相同的效果。如果您想要使用ASCII字符编码,可以直接将十进制数53转换为字符串,例如通过CharToString(53),所得结果相同。可供选择的方式多种多样,具体取决于您的需求。以下是相关代码说明:

我们通过调用“calculateSliderHeight”函数计算“slider_height”,并使用“createButton”函数在“slider_y”处创建“SCROLL_SLIDER”,将其“OBJPROP_WIDTH”设置为2,如果“debugLogging”为true,则记录相关详细信息。随后,我们调用“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”重置为0,并使用ArrayFree函数清空数组“current_eventNames_data”和“current_displayable_eventNames”,为后续更新事件数据做好准备。
在保留测试模式与实盘模式原有筛选逻辑的基础上,我们引入“displayableEvents”数组来存储所有经过筛选的事件,确保可全面访问所有事件(例如1711个事件),解决了原版本仅能显示5-6个事件的问题。在测试模式下,如果“filters_changed”为true,则调用“FilterEventsForTester”函数进行事件筛选,并使用“ArrayFree”函数清空“displayableEvents”数组。随后,通过循环遍历“filteredEvents”,根据“enableTimeFilter”、“enableCurrencyFilter”、“enableImportanceFilter”等筛选条件,将符合条件的事件填充至“displayableEvents”和“current_displayable_eventNames”,并将“totalEvents_Filtered”设置为“ArraySize(displayableEvents)”。
在实盘模式下,我们使用CalendarValueHistory函数获取事件数据;如果“filters_changed”为true,则清空“displayableEvents”数组,随后以类似方式存储经过筛选的事件,并在“debugLogging”启用时记录筛选后的事件数量。
我们通过“isChangeInStringArrays”对比“previous_displayable_eventNames”与“current_displayable_eventNames”,判断事件是否发生变更并设置“events_changed”,同时检查“scroll_pos”与“prev_scroll_pos”是否不同,以确定“scroll_changed”。如果“events_changed”、“filters_changed”或“scroll_changed”任一为true,且“debugLogging”启用,则通过“Print”函数记录变更信息;随后使用ArrayCopy函数更新“previous_displayable_eventNames”数组,并保存当前“scroll_pos”至“prev_scroll_pos”。接着,我们使用ObjectsDeleteAll函数清除“DATA_HOLDERS”和“ARRAY_NEWS”相关的所有UI元素,然后从“displayableEvents”中提取事件:根据“scroll_visible”和“scroll_pos”计算起始索引“start_idx”,确定结束索引“end_idx”(不超过数组大小“ArraySize(displayableEvents)”),最多绘制“VISIBLE_ITEMS”(11个)事件。
对于每个事件,我们调用“createRecLabel”函数创建背景矩形,背景颜色交替使用“C'213,227,207'”(浅绿色)或“clrWhite”;将事件详情填充至“news_data”数组,并使用“createLabel”函数显示各字段,根据事件重要性调整“importance_color”(例如低重要性显示为“clrYellow”);如果“debugLogging”启用,则记录当前显示的事件数量。如果未检测到任何变更,则跳过重绘操作并记录日志,以保持系统性能。
我们通过以下逻辑集成滚动条功能:当“totalEvents_Filtered”超过“VISIBLE_ITEMS”时,设置“new_scroll_visible”为true;如果“scroll_visible”发生变更,或“events_changed”、“filters_changed”为true,则更新“scroll_visible”。随后,使用“createRecLabel”、“createLabel”和“createButton”函数创建滚动条组件(包括“SCROLL_LEADER”、“SCROLL_UP_REC”、“SCROLL_UP_LABEL”、“SCROLL_DOWN_REC”、“SCROLL_DOWN_LABEL”、“SCROLL_SLIDER”),并根据“scroll_pos”与“max_scroll”的关系,将按钮图标颜色设置为“clrBlack”或“clrLightGray”。当不再需要滚动条或相关组件时,使用ObjectDelete函数将其移除;如果“isChangeInStringArrays”检测到“current_eventNames_data”发生变更,则更新“previous_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函数中实现与滚动条相关的新逻辑,显著增强了交互性,使用户能够通过点击和拖动操作无缝浏览新闻事件。我们专注于处理用户与动态滚动条的交互,具体包括处理向上/向下按钮的点击事件以及通过鼠标拖动滑块的操作,确保仪表盘显示内容能够实时响应更新。当"scroll_visible"为true时,我们通过处理CHARTEVENT_OBJECT_CLICK事件来响应滚动条按钮的点击操作。如果被点击的对象("sparam")是"SCROLL_UP_REC"或"SCROLL_UP_LABEL",则调用"scrollUp"函数将"scroll_pos"减1,随后调用"updateButtonColors"函数根据新位置更新按钮图标颜色("clrBlack"或"clrLightGray"),如果"debugLogging"启用则记录操作日志,最后调用ChartRedraw函数刷新界面显示。
同理,当用户点击"SCROLL_DOWN_REC"或"SCROLL_DOWN_LABEL"时,我们调用"scrollDown"函数将"scroll_pos"加1,同样执行颜色更新、日志记录和界面刷新操作,确保仪表盘能够正确显示滚动后的事件列表。相关函数的具体实现逻辑将在后续部分详细说明。
当"scroll_visible"为true且检测到CHARTEVENT_MOUSE_MOVE事件时,我们处理滑块的拖动操作。当"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");将"SCROLL_SLIDER"的背景颜色("OBJPROP_BGCOLOR")改为"clrDodgerBlue",并临时将其高度("OBJPROP_YSIZE")增加2像素;通过ChartSetInteger函数禁用图表自身的滚动功能,防止与滑块拖动产生冲突;如果"debugLogging"启用,则记录拖动开始事件。
当"moving_state_slider"和"mouse_state"均为true(即鼠标按下且正在拖动滑块)时,用当前鼠标坐标"mouse_y"减去初始按下时的坐标"mlb_down_y"计算 "delta_y";通过 MathMax和MathMin函数,将 "new_y"限制在滚动区域的有效范围内:下限"scroll_area_y_min" = "SCROLLBAR_Y" + "BUTTON_SIZE",上限"scroll_area_y_max" = "scroll_area_y_min" + "SCROLL_AREA_HEIGHT" - "slider_height";将滚动滑块对象"SCROLL_SLIDER"的 "OBJPROP_YDISTANCE"设置为"new_y"。我们根据“new_y”在滚动区间内的比例“scroll_ratio”计算出“new_scroll_pos”,如果与“scroll_pos”不同,则更新“scroll_pos”,并通过“curr_filter_selected”与“imp_filter_selected”调用“update_dashboard_values”,之后刷新按钮颜色“updateButtonColors”,记录拖动详情,最后调用ChartRedraw重绘图表。
当 "mouse_state" 变为0(即鼠标释放)时,我们重置"moving_state_slider",将"SCROLL_SLIDER"的"OBJPROP_BGCOLOR"恢复为"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"是否大于0,判断可否向上滚动。如果满足,将"scroll_pos"减1;调用"update_dashboard_values"函数,传入当前筛选条件"curr_filter_selected"和重要筛选条件"imp_filter_selected",以刷新显示的事件列表;根据"updateSliderPosition"调整"SCROLL_SLIDER"的位置;如果启用“调试日志”(debugLogging),则使用Print记录新的"scroll_pos"值。如果"scroll_pos"为0,则记录提示信息,表明已到达列表顶端,避免不必要的更新操作,确保交互流程简洁流畅。
在"scrollDown"函数中,我们实现事件列表的向下滚动导航功能。通过MathMax计算"max_scroll",确保其值为非负数。具体计算方式为:ArraySize(displayableEvents)减去 "VISIBLE_ITEMS"(常量值11),该值表示列表允许的最大滚动位置。
如果"scroll_pos"小于"max_scroll",将"scroll_pos"加1;通过"curr_filter_selected"和"imp_filter_selected"调用"update_dashboard_values"刷新事件列表;使用"updateSliderPosition"重新定位"SCROLL_SLIDER";如果启用调试日志(debugLogging),则使用"Print"记录新的"scroll_pos"值。如果"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) 动态图像,直观地展示仪表盘的实际运行效果,如下方所示:

由可视化效果可见,滚动条功能本身正常,但当通过点击按钮更改筛选条件时,滚动条未能动态更新(尽管显示的事件内容已正确变化)。这一问题源于缺乏重新计算逻辑,导致滚动条无法完全实现动态适配。为实现这一功能,我们需要在每次按钮触发筛选条件变更时,重新校准滚动条。我们也可以通过监听数据变化直接更新滚动条,但是这种方式会在无需更新时产生冗余流程,降低效率。实现该功能的完整逻辑如下:
//+------------------------------------------------------------------+ //| 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动画所示),显著提升了用户体验。这些改进基于第八部分的回测优化,确保在实盘与测试模式下均能实现流畅交互,为新闻驱动型交易策略提供了稳健的平台支撑。您可以将此优化后的仪表盘作为基础框架,根据个性化交易需求进一步定制开发。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18135
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第 23 部分):货币强弱指标
新手在交易中的10个基本错误