MQL5经济日历交易指南(第九部分):通过动态滚动条与界面优化提升新闻交互体验·综合运用
- 财经日历面板的绘制与滚动条逻辑
- 滚动条与面板的对象生命周期
- 事件面板销毁与图表交互接管
- 用滑块接管图表滚动的鼠标事件
- 新闻面板的滚动与心跳逻辑
- 在图表上点一下就改新闻过滤逻辑
- 按钮状态驱动的过滤器实时改写
- 按钮选中态如何写进过滤数组
- 财经事件重要性筛选的动态增删逻辑
- 拖拽滑块与滚动按钮的事件响应
- 滑块拖拽如何映射事件列表滚动
- 筛选按钮触发后滚动条的重算缺口
- 滚动条与重要性过滤按钮的绘制逻辑
- 事件面板滚动条的重建逻辑
- 按钮状态驱动的时间过滤与滚动条重建
- 币种筛选按钮的状态切换逻辑
- 筛选结果落盘与滚动条重建
- 事件影响度按钮的取消筛选逻辑
- 按钮选中后的过滤器数组维护
- 事件面板滚动条的绘制与点击事件
- 自绘滚动条怎么跟着鼠标拖
- 拖拽滑块结束后的状态复位
- 画得少,看得清
财经日历面板的绘制与滚动条逻辑
这段 MT5 脚本负责把筛选后的财经事件画到图表上,并维护一个简易滚动条。先给 news_data[6]、news_data[7] 填入预测值与前值,均保留 3 位小数,方便横向对齐阅读。 遍历 array_calendar 时,下标 k==3 的那一列用 22 号字、按重要性着色,其余列用 12 号黑字;每画完一列 startX 累加 buttons[k]+3 像素,控制列间距。事件名追加进 current_eventNames_data 数组,startY 再叠 ITEM_HEIGHT 进入下一行。 若 debugLogging 开启,会打印 Displayed N events, start_idx=…, end_idx=…;无变动则跳过重绘,省 CPU。时间标签实时显示服务器时间(含日期秒)与 Total News: 过滤后/考虑中 的总数。 滚动条仅在 totalEvents_Filtered > VISIBLE_ITEMS 时出现。新建 SCROLL_LEADER 底框后,按 scroll_pos 算最大滚动量 max_scroll,到顶时向上箭头变 clrLightGray、到底时向下箭头同理,用颜色灰化表达不可点。外汇与贵金属受数据面冲击大,这类面板只作信息辅助,实际波动风险仍高。
news_data[class="num">6] = DoubleToString(displayableEvents[i].forecast, class="num">3); news_data[class="num">7] = DoubleToString(displayableEvents[i].previous, class="num">3); for (class="type">int k = class="num">0; k < ArraySize(array_calendar); k++) { if (k == class="num">3) { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(class="num">22-class="num">12),news_data[k],importance_color,class="num">22,"Calibri"); } else { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,class="num">12,"Calibri"); } startX += buttons[k]+class="num">3; } ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+class="num">1); current_eventNames_data[ArraySize(current_eventNames_data)-class="num">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."); } class=class="str">"cmt">// Update TIME_LABEL class="type">class="kw">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)); class=class="str">"cmt">// Update scrollbar visibility class="type">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(class="num">0, SCROLL_LEADER) < class="num">0) { createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, class="num">1, clrNONE); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">class="kw">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, class="num">1, clrDarkGray);
「滚动条与面板的对象生命周期」
在事件面板里,滚动条不是常驻控件,而是根据过滤后事件数量动态创建或销毁的。上面这段逻辑先算好滑块高度 slider_height,再把上箭头、下箭头、滑块分别用 createLabel / createRecLabel / createButton 画出来;箭头字符取自 Webdings 字体,0x35 是上三角、0x36 是下三角,字体尺寸写死为 15。 滑块本身用 Arial Bold、宽 2 像素的边(OBJPROP_WIDTH=2),颜色在 clrWhite 与 clrLightSlateGray 之间切换。若 debugLogging 开启,终端会打印 totalEvents_Filtered 和 slider_height,方便你确认过滤后条目数是否和滑块长度匹配。 当条件不满足时,代码走 else 分支,依次 ObjectDelete 掉 SCROLL_LEADER、SCROLL_UP_REC、SCROLL_UP_LABEL、SCROLL_DOWN_REC、SCROLL_DOWN_LABEL、SCROLL_SLIDER 六个对象,相当于把整条滚动条拆干净。 destroy_Dashboard() 则更彻底:除了删 MAIN_REC、SUB_REC1、SUB_REC2 三个底板和 HEADER_LABEL、TIME_LABEL、IMPACT_LABEL 文本,还用 ObjectsDeleteAll 按前缀批量清掉 ARRAY_CALENDAR 与 ARRAY_NEWS 两组对象。外汇与贵金属消息面波动大,这类面板对象若不及时释放,MT5 图表对象数可能累积到上限(默认 512),导致后续绘制失败。
createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">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, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, class="num">12, clrLightSlateGray, clrDarkGray, "Arial Bold"); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_WIDTH, class="num">2); if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height); } updateSliderPosition(); updateButtonColors(); } else { ObjectDelete(class="num">0, SCROLL_LEADER); ObjectDelete(class="num">0, SCROLL_UP_REC); ObjectDelete(class="num">0, SCROLL_UP_LABEL); ObjectDelete(class="num">0, SCROLL_DOWN_REC); ObjectDelete(class="num">0, SCROLL_DOWN_LABEL); ObjectDelete(class="num">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); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destroy dashboard | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void destroy_Dashboard() { ObjectDelete(class="num">0,"MAIN_REC"); ObjectDelete(class="num">0,"SUB_REC1"); ObjectDelete(class="num">0,"SUB_REC2"); ObjectDelete(class="num">0,"HEADER_LABEL"); ObjectDelete(class="num">0,"TIME_LABEL"); ObjectDelete(class="num">0,"IMPACT_LABEL"); ObjectsDeleteAll(class="num">0,"ARRAY_CALENDAR"); ObjectsDeleteAll(class="num">0,"ARRAY_NEWS");
◍ 事件面板销毁与图表交互接管
在指标或 EA 退出前,必须先把自建的 GUI 对象全部清掉,否则 MT5 切周期或重加载时会残留不可控的按钮与标签。上面这段收尾代码用 ObjectsDeleteAll 按前缀批量删:"DATA_HOLDERS"、"IMPACT_LABEL"、"CURRENCY_BTNS" 三组一次清空,其余独立控件(过滤按钮、取消键、滚动条部件)走 ObjectDelete 单点移除,最后 ArrayFree 释放三个事件数组并 ChartRedraw 强制重绘。 OnChartEvent 是图表交互的入口。lparam/dparam 在鼠标事件里分别承载 x、y 像素坐标,sparam 在点击时是被点对象名、在移动时则是鼠标按键状态(0=松开,1=按下)。滚动条逻辑里,点了上/下箭头就调 scrollUp/scrollDown 并刷新按钮配色,调试开关打开时会 Print 出当前 scroll_pos——这个位置变量直接决定事件列表的偏移量。 鼠标拖拽滑块的判定藏在 MOUSE_MOVE 分支:只有当 prev_mouse_state 为 0 且当前 mouse_state 为 1,才认为用户刚按下滑块。接着用 ObjectGetInteger 读 SCROLL_SLIDER 的 XDISTANCE、YDISTANCE、XSIZE,拿到滑块相对图表左上角的位置和自身宽度,后面就能算拖拽映射。外汇与贵金属行情受事件冲击大,这类自定义面板帮你快速过滤财经事件,但杠杆交易高风险,任何信号都只是概率倾向。
ObjectsDeleteAll(class="num">0,"DATA_HOLDERS"); ObjectsDeleteAll(class="num">0,"IMPACT_LABEL"); ObjectDelete(class="num">0,"FILTER_LABEL"); ObjectDelete(class="num">0,"FILTER_CURR_BTN"); ObjectDelete(class="num">0,"FILTER_IMP_BTN"); ObjectDelete(class="num">0,"FILTER_TIME_BTN"); ObjectDelete(class="num">0,"CANCEL_BTN"); ObjectsDeleteAll(class="num">0,"CURRENCY_BTNS"); ObjectDelete(class="num">0, SCROLL_LEADER); ObjectDelete(class="num">0, SCROLL_UP_REC); ObjectDelete(class="num">0, SCROLL_UP_LABEL); ObjectDelete(class="num">0, SCROLL_DOWN_REC); ObjectDelete(class="num">0, SCROLL_DOWN_LABEL); ObjectDelete(class="num">0, SCROLL_SLIDER); ArrayFree(displayableEvents); ArrayFree(current_displayable_eventNames); ArrayFree(previous_displayable_eventNames); ChartRedraw(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Chart event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { class="type">int mouse_x = (class="type">int)lparam; class="type">int mouse_y = (class="type">int)dparam; class="type">int mouse_state = (class="type">int)sparam; if (id == CHARTEVENT_OBJECT_CLICK) { class=class="str">"cmt">// 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(class="num">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(class="num">0); } } else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { if (prev_mouse_state == class="num">0 && mouse_state == class="num">1) { class="type">int xd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XDISTANCE); class="type">int yd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE); class="type">int xs = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XSIZE);
用滑块接管图表滚动的鼠标事件
在 MT5 自定义面板里做事件滚动条,核心是把系统鼠标滚轮和拖拽先关掉,再用鼠标坐标判断命中。下面这段逻辑先取滑块的纵向尺寸 ys,若鼠标落在滑块矩形内(xd~xd+xs,yd~yd+ys),就置 moving_state_slider=true,记下按下点,并把滑块底色改成 clrDodgerBlue、高度 +2 像素,同时 ChartSetInteger 关掉 CHART_MOUSE_SCROLL,避免和系统滚动抢事件。 拖拽过程中,用 delta_y = mouse_y - mlb_down_y 算出偏移,new_y 限制在 scroll_area_y_min 到 scroll_area_y_max 之间。scroll_ratio 由 new_y 在区间里的位置除以区间长度得到,再乘 max_scroll(= 可显示事件数 - VISIBLE_ITEMS 与 0 的较大值)并四舍五入,就是新的 scroll_pos。只要位置变了就重算面板数值和按钮色,然后 ChartRedraw。 鼠标松开(mouse_state==0)时,把 moving_state_slider 复位,滑块恢复 clrLightSlateGray 和原高度,重新打开 CHART_MOUSE_SCROLL。外汇与贵金属图表叠加这类自绘控件时,误吞系统滚动会让用户措手不及,建议 debugLogging 留着,能直接 Print 出 Slider y 和 CurrPos 方便核对。
class="type">int ys = (class="type">int)ObjectGetInteger(class="num">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(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + class="num">2); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false); if (debugLogging) Print("Slider drag started at y=", mouse_y); } if (moving_state_slider && mouse_state == class="num">1) { class="type">int delta_y = mouse_y - mlb_down_y; class="type">int new_y = mlb_down_yd_slider + delta_y; class="type">int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE; class="type">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(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">double scroll_ratio = (class="type">class="kw">double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); class="type">int new_scroll_pos = (class="type">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(class="num">0); } if (mouse_state == class="num">0) { if (moving_state_slider) { moving_state_slider = false; ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); if (debugLogging) Print("Slider drag stopped."); ChartRedraw(class="num">0); } }
「新闻面板的滚动与心跳逻辑」
面板里的上下滚动不是简单自增,而是被可见条目数卡死的。scrollDown 用 MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS) 算出最大偏移,只有 scroll_pos 小于这个值才允许加一,否则在调试日志里提示已到底——这一行直接决定了长新闻列表会不会越界刷屏。 scrollUp 则更苛刻,scroll_pos 大于 0 才减一,到顶就停。每次滚动都顺手调 update_dashboard_values 和 updateSliderPosition,滑条位置和事件内容必须同步,不然鼠标拖了滑条面板却不翻页,体验就断了。 OnTick 是面板的心跳。回测环境下(MQLInfoInteger(MQL_TESTER) 为真)它用 TimeTradeServer 拿当前时间,按 range_time 周期算 timeAfter,仅当过滤器有变动或距上次更新超过该周期才重算面板,并用 ArrayFree + ArrayCopy 把事件名存进 last_dashboard_eventNames;实盘则每次都更新。这种节流写法能压住 CPU 占用,尤其外汇和贵金属行情跳得快、回测样本大时,盲目每 tick 刷新可能拖慢 EA。 别把节流周期写死 range_time 如果设太短,回测里面板重绘频率会陡增;设太长又可能错过突发行情。建议先在策略测试器里用 debugLogging 看 Print 出来的 CurrPos 与更新时间戳,再反过来定参数。
class="type">void scrollUp() { if (scroll_pos > class="num">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."); } } class="type">void scrollDown() { class="type">int max_scroll = MathMax(class="num">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); } } class="type">void OnTick() { UpdateFilterInfo(); CheckForNewsTrade(); if (isDashboardUpdate) { if (MQLInfoInteger(MQL_TESTER)) { class="type">class="kw">datetime currentTime = TimeTradeServer(); class="type">class="kw">datetime timeRange = PeriodSeconds(range_time); class="type">class="kw">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); } } }
◍ 在图表上点一下就改新闻过滤逻辑
做新闻面板的 EA,最怕用户没法实时干预。这段代码把图表点击事件接进 OnChartEvent,鼠标点哪个按钮,就当场切换货币过滤或重要性过滤,不用重启 EA。 注意 lparam、dparam、sparam 这三个参数:x/y 坐标拿鼠标位置,sparam 才是按钮名。判断 id == CHARTEVENT_OBJECT_CLICK 后,先跑 UpdateFilterInfo 和 CheckForNewsTrade,再按 sparam 分流到取消键或过滤键。 货币过滤键 FILTER_CURR_BTN 的处理里,用 ObjectGetInteger 读 OBJPROP_STATE 拿到按钮开关状态,写进 enableCurrencyFilter。勾选时文本拼 0x2714(✔)显绿,取消时拼 0x274C(✘)显红,再 ChartRedraw 刷新。若在策略测试器里跑,filters_changed 置 true 让回测感知变动。 重要性过滤键 FILTER_IMP_BTN 逻辑完全一致,只是旗标换成 enableImportanceFilter。两个键都调 update_dashboard_values 重算面板,debug 开时 Print 出状态便于排查。外汇和贵金属受新闻跳空影响大,这类开关能帮你避开高影响时段,但行情仍可能反向,属高风险操作。
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam) { class="type">int mouse_x = (class="type">int)lparam; class="type">int mouse_y = (class="type">int)dparam; class="type">int mouse_state = (class="type">int)sparam; if (id == CHARTEVENT_OBJECT_CLICK) { UpdateFilterInfo(); CheckForNewsTrade(); if (sparam == CANCEL_BTN) { isDashboardUpdate = false; destroy_Dashboard(); } if (sparam == FILTER_CURR_BTN) { class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); enableCurrencyFilter = btn_state; if (debugLogging) Print(sparam+" STATE = "+(class="type">class="kw">string)btn_state+", FLAG = "+(class="type">class="kw">string)enableCurrencyFilter); class="type">class="kw">string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency"; class="type">class="kw">color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed; ObjectSetString(class="num">0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text); ObjectSetInteger(class="num">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: "+(class="type">class="kw">string)enableCurrencyFilter); ChartRedraw(class="num">0); } if (sparam == FILTER_IMP_BTN) { class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); enableImportanceFilter = btn_state; if (debugLogging) Print(sparam+" STATE = "+(class="type">class="kw">string)btn_state+", FLAG = "+(class="type">class="kw">string)enableImportanceFilter); class="type">class="kw">string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance"; class="type">class="kw">color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed; ObjectSetString(class="num">0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text); ObjectSetInteger(class="num">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);
按钮状态驱动的过滤器实时改写
在 MT5 自定义面板里,时间过滤按钮(FILTER_TIME_BTN)被点击后,先通过 ObjectGetInteger 读取 OBJPROP_STATE 拿到布尔值,直接赋给 enableTimeFilter。若处于回测环境(MQLInfoInteger(MQL_TESTER) 为真),会把 filters_changed 置 true,让下一根 K 线重新评估品种过滤条件。 按钮文字与颜色随状态切换:选中时拼出 Unicode 0x2714(✔)加 "Time" 且用 clrLime,未选中则用 0x274C(✖)加 "Time" 且用 clrRed。这两行 ObjectSetString / ObjectSetInteger 改完必须接 ChartRedraw(0),否则界面不会刷新。 货币对按钮的处理更绕:用 StringFind 命中 CURRENCY_BTNS 前缀后,先取按钮文本作 selected_curr。当 btn_state 为 false(取消选中),代码在 curr_filter_selected 数组里线性查找并删除该项——靠双层 for 把后方元素前移一格,再 ArrayResize 缩容 1。外汇与贵金属波动受时段与品种交集影响明显,这类交互过滤在高杠杆下属于高风险操作,实盘前建议在策略测试器跑通逻辑。 调试开关 debugLogging 贯穿全程,每次状态变更都 Print 出 STATE 与 FLAG,开 MT5 终端看专家日志就能确认数组增删是否如预期。
if (debugLogging) Print("Success. Changes updated! State: "+ (class="type">class="kw">string)enableImportanceFilter); ChartRedraw(class="num">0); } if (sparam == FILTER_TIME_BTN) { class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); enableTimeFilter = btn_state; if (debugLogging) Print(sparam+" STATE = "+(class="type">class="kw">string)btn_state+", FLAG = "+(class="type">class="kw">string)enableTimeFilter); class="type">class="kw">string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time"; class="type">class="kw">color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed; ObjectSetString(class="num">0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text); ObjectSetInteger(class="num">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: "+(class="type">class="kw">string)enableTimeFilter); ChartRedraw(class="num">0); } if (StringFind(sparam,CURRENCY_BTNS) >= class="num">0) { class="type">class="kw">string selected_curr = ObjectGetString(class="num">0,sparam,OBJPROP_TEXT); if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr); class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); if (btn_state == false) { if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE."); for (class="type">int i = class="num">0; i < ArraySize(curr_filter_selected); i++) { if (curr_filter_selected[i] == selected_curr) { for (class="type">int j = i; j < ArraySize(curr_filter_selected) - class="num">1; j++) { curr_filter_selected[j] = curr_filter_selected[j + class="num">1]; } ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - class="num">1); if (debugLogging) Print("Removed from selected filters: ", selected_curr); class="kw">break; } } } else {
「按钮选中态如何写进过滤数组」
这段逻辑处理的是面板按钮被点击后,如何把币种或重要性级别塞进对应的过滤数组。先判断 debugLogging 开关,开着就在终端打印「BUTTON IS IN SELECTED MODE. TAKE ACTION」,方便你直接在 MT5 策略测试器里看事件流。 随后用一个布尔变量 already_selected 标记该币种是否已在 curr_filter_selected 里。for 循环遍历数组,若命中则置 true 并 break,避免无谓扫描——数组长度由 ArraySize 实时返回,不依赖外部计数。 若未选中,则 ArrayResize 把数组加 1,再把 selected_curr 写进末位;已选中就只打一行日志。注意 ArrayResize 后索引要用 ArraySize-1,新手常写成固定下标导致越界。 调试收尾处会打印 SELECTED ARRAY SIZE 并用 ArrayPrint dump 整个数组;若在回测环境(MQLInfoInteger(MQL_TESTER) 为真)则把 filters_changed 置 true,触发后续 update_dashboard_values 重算并 ChartRedraw(0) 强制重绘。外汇与贵金属事件驱动面板高频重绘,实盘注意 GPU/CPU 占用风险。 重要性按钮分支用 StringFind 匹配 IMPACT_LABEL,取出按钮文本后由 get_importance_level 映射成枚举;OBJPROP_STATE 为 false 表示取消选中,此时边框改 clrBlack 并进入移除循环。
if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION"); class="type">bool already_selected = false; for (class="type">int j = class="num">0; j < ArraySize(curr_filter_selected); j++) { if (curr_filter_selected[j] == selected_curr) { already_selected = true; class="kw">break; } } if (!already_selected) { ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + class="num">1); curr_filter_selected[ArraySize(curr_filter_selected) - class="num">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(class="num">0); if (StringFind(sparam, IMPACT_LABEL) >= class="num">0) { class="type">class="kw">string selected_imp = ObjectGetString(class="num">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,")"); class="type">bool btn_state = ObjectGetInteger(class="num">0, sparam, OBJPROP_STATE); class="type">class="kw">color color_border = btn_state ? clrNONE : clrBlack; if (btn_state == false) { if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE."); for (class="type">int i = class="num">0; i < ArraySize(imp_filter_selected); i++) { if (impact_filter_selected[i] == selected_imp) {
◍ 财经事件重要性筛选的动态增删逻辑
在 MT5 的 EA 或指标里做新闻过滤器时,用户点一下界面按钮,后台就得在已选重要性数组里增删对应项。上面这段逻辑处理了两个方向:按钮处于「已选」状态时把该项移除,处于「未选」状态时把该项加入,且加入前先遍历确认不重复。 移除时用 for 循环从当前位置 i 开始,把后面每个元素往前挪一位,覆盖掉被删项;随后两次 ArrayResize 把两个平行数组(imp_filter_selected 与 impact_filter_selected)长度各减 1。若开启 debugLogging,会打印出被移除的具体重要性标识与等级。 加入分支先声明 already_selected 并跑一遍现有数组做存在性检查。确认未选才把两个数组各扩容 1,把 selected_importance_lvl 和 selected_imp 写到末位。这样能保证数组里同一重要性等级只出现一次,避免重复触发。 外汇与贵金属受新闻冲击大、滑点风险高,这类过滤数组建议配合 debug 打印验证:每次点击后 ArraySize 应输出一致长度,否则两个平行数组错位会让后续新闻屏蔽判断失效。
for (class="type">int j = i; j < ArraySize(imp_filter_selected) - class="num">1; j++) { imp_filter_selected[j] = imp_filter_selected[j + class="num">1]; impact_filter_selected[j] = impact_filter_selected[j + class="num">1]; } ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - class="num">1); ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - class="num">1); if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")"); class="kw">break; } } } else { if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION"); class="type">bool already_selected = false; for (class="type">int j = class="num">0; j < ArraySize(imp_filter_selected); j++) { if (impact_filter_selected[j] == selected_imp) { already_selected = true; class="kw">break; } } if (!already_selected) { ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + class="num">1); imp_filter_selected[ArraySize(imp_filter_selected) - class="num">1] = selected_importance_lvl; ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + class="num">1); impact_filter_selected[ArraySize(impact_filter_selected) - class="num">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);
拖拽滑块与滚动按钮的事件响应
面板里的滚动条不是摆设。鼠标按下瞬间先判 hitbox:用 ObjectGetInteger 抓 SCROLL_SLIDER 的 XDISTANCE/YDISTANCE 和 XSIZE/YSIZE,只有点击坐标落在滑块矩形内才把 moving_state_slider 置真,同时记录按下点 mlb_down_x/y 和滑块初始 YD,并把背景刷成 clrDodgerBlue、高度 +2 像素给出视觉反馈。 顺带把 CHART_MOUSE_SCROLL 关掉,否则图表会跟着鼠标滚轮乱跑,拖拽体验直接废掉。 上下滚动按钮走的是另一套逻辑:sparam 命中 SCROLL_UP_REC / SCROLL_UP_LABEL 就调 scrollUp(),命中 DOWN 系列就 scrollDown(),每次都刷按钮颜色并 ChartRedraw(0)。debug 模式下会 Print 出 CurrPos,方便你确认滚动位置对不对。 拖拽过程里 delta_y = mouse_y - mlb_down_y,new_y = mlb_down_yd_slider + delta_y,这就是滑块跟随鼠标的位移基准。外汇与贵金属行情跳变快,这类自定义面板在高波动时可能漏事件,建议你在策略测试器里开 MQL_TESTER 跑一遍 filters_changed 的实际触发。
if(debugLogging) ArrayPrint(impact_filter_selected); if(MQLInfoInteger(MQL_TESTER)) filters_changed = true; update_dashboard_values(curr_filter_selected,imp_filter_selected); ObjectSetInteger(class="num">0,sparam,OBJPROP_BORDER_COLOR,color_border); if(debugLogging) Print("SUCCESS. DASHBOARD UPDATED"); ChartRedraw(class="num">0); } class=class="str">"cmt">// 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(class="num">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(class="num">0); } } else if(id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { if(prev_mouse_state == class="num">0 && mouse_state == class="num">1) { class="type">int xd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XDISTANCE); class="type">int yd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE); class="type">int xs = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XSIZE); class="type">int ys = (class="type">int)ObjectGetInteger(class="num">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(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + class="num">2); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false); if(debugLogging) Print("Slider drag started at y=", mouse_y); } } if(moving_state_slider && mouse_state == class="num">1) { class="type">int delta_y = mouse_y - mlb_down_y; class="type">int new_y = mlb_down_yd_slider + delta_y;
「滑块拖拽如何映射事件列表滚动」
这段逻辑处理 MT5 自定义面板里滚动滑块的拖动与释放。滑块只能在滚动区范围内移动,范围由 SCROLLBAR_Y + BUTTON_SIZE 起算,到该值加 SCROLL_AREA_HEIGHT 再减去滑块高度为止。
class="type">int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE; class="type">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(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">double scroll_ratio = (class="type">class="kw">double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); class="type">int new_scroll_pos = (class="type">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(class="num">0); } if (mouse_state == class="num">0) { if (moving_state_slider) { moving_state_slider = false; ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); if (debugLogging) Print("Slider drag stopped."); ChartRedraw(class="num">0); } } prev_mouse_state = mouse_state; } }
class="type">int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE; class="type">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(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">double scroll_ratio = (class="type">class="kw">double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); class="type">int new_scroll_pos = (class="type">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(class="num">0); } if (mouse_state == class="num">0) { if (moving_state_slider) { moving_state_slider = false; ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); if (debugLogging) Print("Slider drag stopped."); ChartRedraw(class="num">0); } } prev_mouse_state = mouse_state; } }
◍ 筛选按钮触发后滚动条的重算缺口
仪表盘优化完后跑了实盘与策略测试器两种模式的验证,重点看滚动条视觉反馈、筛选事件完整显示和静默更新是否卡顿。录屏里能直接看到:滚动条本身滑动正常,但点按钮换筛选条件时,事件内容变了,滚动条却没跟着重算——根因是缺少按钮侧的重新校准逻辑。 如果改成监听数据变化去被动刷新滚动条,会在不需要更新时跑冗余流程,拖慢效率。更直接的做法是把原本挂在滚动条元素点击上的重算,扩展到每个独立筛选按钮的点击事件里。 编译后再录一次,所有筛选按钮切换时滚动条都实时重算,事件区显示也更顺。外汇与贵金属图表挂这种自定义 OBJ_LABEL 仪表盘时,要注意高频点击可能触发重绘抖动,属于高风险交互改造,建议先在策略测试器跑满 200 根以上 K 线观察。 下面这段是筛选货币按钮后的核心处理:删掉旧滚动条部件、按过滤后事件数决定显隐、再重建。VISIBLE_ITEMS 是单屏可显示条数,totalEvents_Filtered 超过它才画滚动条。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Chart event handler |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnChartEvent</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> id, <span class="keyword">const</span> <span class="keyword">class="type">long</span>& lparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span>& dparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span>& sparam) { <span class="keyword">class="type">int</span> mouse_x = (<span class="keyword">class="type">int</span>)lparam; <span class="keyword">class="type">int</span> mouse_y = (<span class="keyword">class="type">int</span>)dparam; <span class="keyword">class="type">int</span> mouse_state = (<span class="keyword">class="type">int</span>)sparam; <span class="keyword">if</span> (id == <span class="macro">CHARTEVENT_OBJECT_CLICK</span>) { UpdateFilterInfo(); CheckForNewsTrade(); <span class="keyword">if</span> (sparam == CANCEL_BTN) { isDashboardUpdate = <span class="macro">false</span>; destroy_Dashboard(); } <span class="keyword">if</span> (sparam == FILTER_CURR_BTN) { <span class="keyword">class="type">bool</span> btn_state = <span class="functions">ObjectGetInteger</span>(<span class="number">class="num">0</span>,sparam,<span class="macro">OBJPROP_STATE</span>); enableCurrencyFilter = btn_state; <span class="keyword">if</span> (debugLogging) <span class="functions">Print</span>(sparam+<span class="class="type">class="kw">string">" STATE = "</span>+(<span class="keyword">class="type">class="kw">string</span>)btn_state+<span class="class="type">class="kw">string">", FLAG = "</span>+(<span class="keyword">class="type">class="kw">string</span>)enableCurrencyFilter); <span class="keyword">class="type">class="kw">string</span> filter_curr_text = enableCurrencyFilter ? <span class="functions">ShortToString</span>(<span class="number">0x2714</span>)+<span class="class="type">class="kw">string">"Currency"</span> : <span class="functions">ShortToString</span>(<span class="number">0x274C</span>)+<span class="class="type">class="kw">string">"Currency"</span>; <span class="keyword">class="type">class="kw">color</span> filter_curr_txt_color = enableCurrencyFilter ? <span class="macro">clrLime</span> : <span class="macro">clrRed</span>; <span class="functions">ObjectSetString</span>(<span class="number">class="num">0</span>,FILTER_CURR_BTN,<span class="macro">OBJPROP_TEXT</span>,filter_curr_text); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,FILTER_CURR_BTN,<span class="macro">OBJPROP_COLOR</span>,filter_curr_txt_color); <span class="keyword">if</span> (<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>)) filters_changed = <span class="macro">true</span>; update_dashboard_values(curr_filter_selected,imp_filter_selected); <span class="comment">class=class="str">"cmt">// Recalculate scrollbar</span> <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_LEADER); <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_UP_REC); <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_UP_LABEL); <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_DOWN_REC); <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_DOWN_LABEL); <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_SLIDER); scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS; <span class="keyword">if</span> (debugLogging) <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Scrollbar visibility: "</span>, scroll_visible ? <span class="class="type">class="kw">string">"Visible"</span> : <span class="class="type">class="kw">string">"Hidden"</span>); <span class="keyword">if</span> (scroll_visible) { createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, <span class="macro">clrSilver</span>, <span class="number">class="num">1</span>, <span class="macro">clrNONE</span>);
滚动条与重要性过滤按钮的绘制逻辑
这段片段负责在 MT5 图表上动态生成财经事件面板的滚动条,以及切换重要性过滤开关时的界面反馈。先算可滚动上限 max_scroll,它等于事件数组长度减去常量 VISIBLE_ITEMS(可见条目数),若数组不足一屏则取 0,避免向下越界。 上下箭头用 Webdings 字体画:向上箭头取字符码 0x35、向下取 0x36,当 scroll_pos 处于顶端或底端时把颜色置为 clrLightGray 表示禁用,否则 clrBlack。滑块高度由 calculateSliderHeight() 算出来,再贴到 SCROLLBAR_Y + BUTTON_SIZE 的位置,宽度写死 2 像素边框。 重要性过滤按钮被点击时,读 OBJPROP_STATE 拿到布尔值赋给 enableImportanceFilter;文案用 0x2714(✔)或 0x274C(✘)拼 "Importance",开态染 clrLime、关态染 clrRed。若在策略测试器里跑,直接把 filters_changed 标 true 触发重算。 调试分支里 Print 会吐出 totalEvents_Filtered 与 slider_height 两个数值,挂真盘前把 debugLogging 打开,能在专家日志里核对滑块是否随事件数线性变化,外汇与贵金属事件面板的高频刷新本身带有终端卡顿风险,参数别乱改。
class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">class="kw">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, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">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, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, class="num">12, clrLightSlateGray, clrDarkGray, "Arial Bold"); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_WIDTH, class="num">2); if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height); updateSliderPosition(); updateButtonColors(); } if (debugLogging) Print("Success. Changes updated! State: "+(class="type">class="kw">string)enableCurrencyFilter); ChartRedraw(class="num">0); } if (sparam == FILTER_IMP_BTN) { class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); enableImportanceFilter = btn_state; if (debugLogging) Print(sparam+" STATE = "+(class="type">class="kw">string)btn_state+", FLAG = "+(class="type">class="kw">string)enableImportanceFilter); class="type">class="kw">string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance"; class="type">class="kw">color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed; ObjectSetString(class="num">0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text); ObjectSetInteger(class="num">0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color); if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
「事件面板滚动条的重建逻辑」
在刷新财经事件面板时,先调用 update_dashboard_values 按当前筛选条件重算显示内容,随后必须把旧滚动条相关对象全部删掉再重建,否则 MT5 图表上会残留上一轮的矩形与标签。 代码里用 ObjectDelete 连续移除 SCROLL_LEADER、SCROLL_UP_REC、SCROLL_UP_LABEL、SCROLL_DOWN_REC、SCROLL_DOWN_LABEL、SCROLL_SLIDER 共 6 个图形对象,这是滚动条重绘的前置清理动作。 scroll_visible 由 totalEvents_Filtered > VISIBLE_ITEMS 决定:只有当过滤后事件总数超过单屏可见条数时才画滚动条。若开启 debugLogging,终端会打印 Visible 或 Hidden 以及 totalEvents_Filtered、slider_height 等数值,方便你直接核对面板状态。 滚动条可见时,向上箭头用 Webdings 字体 0x35 字符、向下箭头用 0x36 字符;当 scroll_pos 已到顶或到底,对应箭头颜色降为 clrLightGray 表示不可点。滑块高度由 calculateSliderHeight() 算得,最后用 ObjectSetInteger 把滑块边框宽度设为 2 像素,再 updateSliderPosition 与 updateButtonColors 同步位置与配色,ChartRedraw 完成重绘。外汇与贵金属事件受突发新闻影响大,面板筛选仅作信息辅助,实际波动高风险。
update_dashboard_values(curr_filter_selected,imp_filter_selected); class=class="str">"cmt">// Recalculate scrollbar ObjectDelete(class="num">0, SCROLL_LEADER); ObjectDelete(class="num">0, SCROLL_UP_REC); ObjectDelete(class="num">0, SCROLL_UP_LABEL); ObjectDelete(class="num">0, SCROLL_DOWN_REC); ObjectDelete(class="num">0, SCROLL_DOWN_LABEL); ObjectDelete(class="num">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, class="num">1, clrNONE); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">class="kw">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, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">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, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, class="num">12, clrLightSlateGray, clrDarkGray, "Arial Bold"); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_WIDTH, class="num">2); if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height); updateSliderPosition(); updateButtonColors(); } if (debugLogging) Print("Success. Changes updated! State: "+(class="type">class="kw">string)enableImportanceFilter); ChartRedraw(class="num">0); } if (sparam == FILTER_TIME_BTN) {
◍ 按钮状态驱动的时间过滤与滚动条重建
在 MT5 图表面板里点一下时间过滤按钮,后台先读 OBJPROP_STATE 拿到布尔值,直接赋给 enableTimeFilter。这个标志位决定后面事件流要不要按交易时段裁剪,属于面板交互的核心开关。
调试模式下会 Print 出按钮名、状态值和标志位,方便在策略测试器里看交互是否生效;实盘外汇或贵金属波动大,这类 UI 状态误触可能让过滤逻辑整体偏移,建议先在模拟盘验证。
按钮文案和颜色随状态切换:勾选时显示 0x2714(✔)配 clrLime,取消时显示 0x274C(✘)配 clrRed,交易者扫一眼就知道当前过滤开没开。若在回测环境(MQL_TESTER)则置 filters_changed,触发面板数值刷新。
滚动条每次都先删后建:把 leader、上下箭头、滑块等 6 个对象 ObjectDelete 清掉,再依据 totalEvents_Filtered > VISIBLE_ITEMS 判定是否显示。实测当过滤后事件数超过可见行数才会重画,否则隐藏以省资源。
可见时按 max_scroll = ArraySize(displayableEvents) - VISIBLE_ITEMS 算最大位移,箭头到顶或到底就染成 clrLightGray 表示不可点;滑块高度由 calculateSliderHeight() 动态得出,这套逻辑直接决定长事件列表能不能顺手翻。
class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); enableTimeFilter = btn_state; if (debugLogging) Print(sparam+" STATE = "+(class="type">class="kw">string)btn_state+", FLAG = "+(class="type">class="kw">string)enableTimeFilter); class="type">class="kw">string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time"; class="type">class="kw">color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed; ObjectSetString(class="num">0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text); ObjectSetInteger(class="num">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); class=class="str">"cmt">// Recalculate scrollbar ObjectDelete(class="num">0, SCROLL_LEADER); ObjectDelete(class="num">0, SCROLL_UP_REC); ObjectDelete(class="num">0, SCROLL_UP_LABEL); ObjectDelete(class="num">0, SCROLL_DOWN_REC); ObjectDelete(class="num">0, SCROLL_DOWN_LABEL); ObjectDelete(class="num">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, class="num">1, clrNONE); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">class="kw">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, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">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, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight();
币种筛选按钮的状态切换逻辑
这段代码片段处理的是图表上币种过滤按钮被点击后的响应。当按钮处于未选中状态时,程序会在 curr_filter_selected 数组里线性查找对应币种字符串,找到后把后续元素整体前移一位,再调用 ArrayResize 把数组长度减 1,相当于把该币种从过滤集合中剔除。 如果按钮变成选中态,代码先置 already_selected 为 false,再遍历 curr_filter_selected 检查是否早已存在该币种。这种前置查重能避免重复写入,否则过滤数组会出现脏数据导致后续事件统计偏差。 调试开关 debugLogging 贯穿全程,会在终端打印按钮名、币种名及移除/选中动作。实盘前建议把该开关设为 false,否则高频点击会让日志刷屏拖慢 MT5 的图表重绘效率。 外汇与贵金属品种波动剧烈、杠杆风险高,这类 UI 过滤仅改变信息展示范围,不预示任何方向,参数验证请在模拟环境先跑通。
class="type">int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, class="num">12, clrLightSlateGray, clrDarkGray, "Arial Bold"); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_WIDTH, class="num">2); if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height); updateSliderPosition(); updateButtonColors(); } if (debugLogging) Print("Success. Changes updated! State: "+ (class="type">class="kw">string)enableTimeFilter); ChartRedraw(class="num">0); } if (StringFind(sparam,CURRENCY_BTNS) >= class="num">0) { class="type">class="kw">string selected_curr = ObjectGetString(class="num">0,sparam,OBJPROP_TEXT); if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr); class="type">bool btn_state = ObjectGetInteger(class="num">0,sparam,OBJPROP_STATE); if (btn_state == false) { if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE."); for (class="type">int i = class="num">0; i < ArraySize(curr_filter_selected); i++) { if (curr_filter_selected[i] == selected_curr) { for (class="type">int j = i; j < ArraySize(curr_filter_selected) - class="num">1; j++) { curr_filter_selected[j] = curr_filter_selected[j + class="num">1]; } ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - class="num">1); if (debugLogging) Print("Removed from selected filters: ", selected_curr); class="kw">break; } } } else { if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION"); class="type">bool already_selected = false; for (class="type">int j = class="num">0; j < ArraySize(curr_filter_selected); j++) { if (curr_filter_selected[j] == selected_curr) { already_selected = true; class="kw">break;
「筛选结果落盘与滚动条重建」
当货币对未出现在已选数组里时,代码先用 ArrayResize 把 curr_filter_selected 扩容 1,再把 selected_curr 塞进末尾;若已存在则只打一行调试日志,避免重复添加导致面板计数失真。 调试开关打开时,会打印 SELECTED ARRAY SIZE 并调用 ArrayPrint 把整个数组铺出来,你在策略测试器里跑一遍就能直接看到选中了几个品种。 紧接着 MQLInfoInteger(MQL_TESTER) 判断若在回测环境就把 filters_changed 置 true,然后调 update_dashboard_values 刷新仪表盘数值,这一步决定了面板是否重绘。 之后连续 ObjectDelete 清掉旧滚动条相关的 6 个图形对象(SCROLL_LEADER、上下按钮、滑块等),再根据 totalEvents_Filtered > VISIBLE_ITEMS 决定 scroll_visible;只有过滤后事件数超过单屏显示上限才重建滚动条。 重建时先计算 max_scroll = ArraySize(displayableEvents) - VISIBLE_ITEMS,到顶时上箭头染浅灰、到底时下箭头染浅灰,用 Webdings 字体 0x35 字符画箭头,位置由 SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE 往下推。
}
}
if (!already_selected) {
ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + class="num">1);
curr_filter_selected[ArraySize(curr_filter_selected) - class="num">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);
class=class="str">"cmt">// Recalculate scrollbar
ObjectDelete(class="num">0, SCROLL_LEADER);
ObjectDelete(class="num">0, SCROLL_UP_REC);
ObjectDelete(class="num">0, SCROLL_UP_LABEL);
ObjectDelete(class="num">0, SCROLL_DOWN_REC);
ObjectDelete(class="num">0, SCROLL_DOWN_LABEL);
ObjectDelete(class="num">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, class="num">1, clrNONE);
class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack;
class="type">class="kw">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, class="num">1, clrDarkGray);
createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings");
class="type">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, class="num">1, clrDarkGray);◍ 事件影响度按钮的取消筛选逻辑
在 MT5 自定义日历面板里,影响度(Importance)按钮被再次点击时会进入未选中态,此时需要从已选筛选数组中剔除对应等级。代码通过比对 impact_filter_selected 与按钮文本 selected_imp,定位下标后做前向覆盖并 ArrayResize 缩容,保证后续拉取事件时不再包含该影响度。 具体实现里,btn_state 为 false 代表用户取消勾选,外层循环扫 imp_filter_selected,命中后内层循环把后续元素整体前移一位,两个并行数组 imp_filter_selected 与 impact_filter_selected 同步缩容 1。若开启 debugLogging,终端会打印 Removed from selected importance filters 及等级名与枚举值,方便你直接在策略测试器里核对数组变化。 这套逻辑对外汇与贵金属交易者意味着:高频低影响数据(如某些国家微调预期)可一键屏蔽,面板只留 CPI、非农、利率决议等可能驱动波动的事件。但贵金属与外汇杠杆高,事件窗口流动性突变仍可能滑点放大,筛选仅降低噪音、不消除风险。 调试时建议先把 debugLogging 置 true,点两次同个影响度按钮,看 Experts 标签里数组长度是否从 N 变 N-1;若没生效,优先查 impact_labels 与 allowed_importance_levels 的映射是否和按钮文本严格一致。
if(StringFind(sparam, IMPACT_LABEL) >= class="num">0) { class="type">class="kw">string selected_imp = ObjectGetString(class="num">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,")"); class="type">bool btn_state = ObjectGetInteger(class="num">0, sparam, OBJPROP_STATE); class="type">class="kw">color color_border = btn_state ? clrNONE : clrBlack; if(btn_state == false) { if(debugLogging) Print("BUTTON IS IN UN-SELECTED MODE."); for(class="type">int i = class="num">0; i < ArraySize(imp_filter_selected); i++) { if(impact_filter_selected[i] == selected_imp) { for(class="type">int j = i; j < ArraySize(imp_filter_selected) - class="num">1; j++) { imp_filter_selected[j] = imp_filter_selected[j + class="num">1]; impact_filter_selected[j] = impact_filter_selected[j + class="num">1]; } ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - class="num">1); ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - class="num">1); if(debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
按钮选中后的过滤器数组维护
这段逻辑处理的是面板按钮处于「已选中」状态时的动作:把当前重要性级别写进两个并行数组,避免重复添加。 先跑一个 for 循环扫 imp_filter_selected,用 impact_filter_selected[j] 比对 selected_imp,命中就置 already_selected=true 并 break,说明该档位已经在过滤列表里。 若未选中,则两次 ArrayResize 分别把 selected_importance_lvl 和 selected_imp 追加到 imp_filter_selected、impact_filter_selected 尾部——这两个数组靠下标对齐存「级别数值」和「标签」。debug 模式会 Print 出 Added to selected importance filters 及具体值。 末尾统一在测试环境(MQLInfoInteger(MQL_TESTER) 为真)置 filters_changed=true,调 update_dashboard_values 刷新面板,并删掉 6 个滚动条相关对象(SCROLL_LEADER 到 SCROLL_SLIDER)后按 totalEvents_Filtered>VISIBLE_ITEMS 重算 scroll_visible。外汇与贵金属事件过滤涉及高风险,实盘前请在策略测试器跑通该分支。
class="kw">break; } } } else { if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION"); class="type">bool already_selected = false; for (class="type">int j = class="num">0; j < ArraySize(imp_filter_selected); j++) { if (impact_filter_selected[j] == selected_imp) { already_selected = true; class="kw">break; } } if (!already_selected) { ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + class="num">1); imp_filter_selected[ArraySize(imp_filter_selected) - class="num">1] = selected_importance_lvl; ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + class="num">1); impact_filter_selected[ArraySize(impact_filter_selected) - class="num">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(class="num">0,sparam,OBJPROP_BORDER_COLOR,color_border); class=class="str">"cmt">// Recalculate scrollbar ObjectDelete(class="num">0, SCROLL_LEADER); ObjectDelete(class="num">0, SCROLL_UP_REC); ObjectDelete(class="num">0, SCROLL_UP_LABEL); ObjectDelete(class="num">0, SCROLL_DOWN_REC); ObjectDelete(class="num">0, SCROLL_DOWN_LABEL); ObjectDelete(class="num">0, SCROLL_SLIDER); scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
「事件面板滚动条的绘制与点击事件」
在 MT5 自定义 dashboard 里,滚动条不是系统控件,而是用矩形标签和文字对象拼出来的。当 scroll_visible 为真时,代码先打一条调试日志,再调用 createRecLabel 以 clrSilver 画外框,宽高由 SCROLLBAR_WIDTH / SCROLLBAR_HEIGHT 常量决定。 可滚动的最大步数由 max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS) 算出。假设 displayableEvents 有 50 条、VISIBLE_ITEMS 为 10,那 max_scroll 就是 40,意味着滑块最多往下走 40 格。 上下箭头用 Webdings 字体画:0x35 是上三角、0x36 是下三角。到顶时 up_color 变 clrLightGray(灰掉),到底时 down_color 同样灰掉,用颜色状态告诉交易者已无更多内容。滑块高度由 calculateSliderHeight() 动态算,再挂一个空文字的 button 对象当拖柄。 点击分支很直白:sparam 命中 SCROLL_UP_REC / SCROLL_UP_LABEL 就跑 scrollUp(),命中下方对象就 scrollDown(),每次点完都 updateButtonColors() 并重绘图表。外汇与贵金属行情瞬息万变,这类面板只做信息聚合,任何信号都只是概率参考,实盘须自担高风险。 调试日志里那句 SUCCESS. DASHBOARD UPDATED 是定位卡顿的锚点——若你改完布局却看不到滚动条,先开 debugLogging 看 max_scroll 与 slider_height 是否非 0,再查 SCROLLBAR_X/Y 有没有被别的标签挡住。
if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden"); if (scroll_visible) { createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, class="num">1, clrNONE); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">class="kw">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, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">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, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = SCROLLBAR_Y + BUTTON_SIZE; createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, class="num">12, clrLightSlateGray, clrDarkGray, "Arial Bold"); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_WIDTH, class="num">2); if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height); updateSliderPosition(); updateButtonColors(); } if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED"); ChartRedraw(class="num">0); class=class="str">"cmt">// 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(class="num">0); } if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) { scrollDown(); updateButtonColors(); if (debugLogging) Print("Down button clicked(", sparam, "). CurrPos: ", scroll_pos);
◍ 自绘滚动条怎么跟着鼠标拖
在 MT5 自定义面板里,系统自带的图表鼠标滚动会和自绘滚动条抢事件,所以拖滑块前必须先关掉 CHART_MOUSE_SCROLL。代码里用 CHARTEVENT_MOUSE_MOVE 配合 mouse_state 从 0 变 1 的瞬间,判定点击是否落在 SCROLL_SLIDER 的包围盒内(xd/xs/yd/ys 四个参数框定),命中才把 moving_state_slider 置真,并把滑块底色切成 clrDodgerBlue、高度 +2 像素做抓取反馈。 拖动过程中用 delta_y = mouse_y - mlb_down_y 算位移,new_y 先加后做 MathMax/MathMin 夹取,边界是 SCROLLBAR_Y + BUTTON_SIZE 到该值加 SCROLL_AREA_HEIGHT 减 slider_height。这一步保证滑块不会拖出轨道——外汇与贵金属行情波动剧烈时面板频繁重绘,越界会直接让索引错乱。 滑块位置到数据偏移量是线性映射:scroll_ratio 用 new_y 在轨道内的占比算出,乘上 max_scroll(即 ArraySize(displayableEvents) - VISIBLE_ITEMS 与 0 的较大者)再四舍五入得 new_scroll_pos。仅当新位置不同于旧 scroll_pos 才刷新仪表盘与按钮色,避免每像素重算。debug 开时 Print 出 CurrPos / Total steps / Slider y,开 MT5 把 debugLogging 设 true 就能看到拖拽全程数值。
ChartRedraw(class="num">0); } } else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) { if (prev_mouse_state == class="num">0 && mouse_state == class="num">1) { class="type">int xd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XDISTANCE); class="type">int yd = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE); class="type">int xs = (class="type">int)ObjectGetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XSIZE); class="type">int ys = (class="type">int)ObjectGetInteger(class="num">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(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + class="num">2); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false); if (debugLogging) Print("Slider drag started at y=", mouse_y); } } if (moving_state_slider && mouse_state == class="num">1) { class="type">int delta_y = mouse_y - mlb_down_y; class="type">int new_y = mlb_down_yd_slider + delta_y; class="type">int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE; class="type">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(class="num">0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">class="kw">double scroll_ratio = (class="type">class="kw">double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); class="type">int new_scroll_pos = (class="type">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(class="num">0); } if (mouse_state == class="num">0) {
拖拽滑块结束后的状态复位
这段逻辑处理的是自定义滚动滑块拖拽结束的瞬间。当 moving_state_slider 为真、且鼠标按键已松开时,代码把该标志位复位为 false,意味着滑块不再跟随鼠标移动。 随后通过 ObjectSetInteger 将滑块背景色改回 clrLightSlateGray,并把 Y 轴尺寸恢复为 slider_height 变量的值,避免拖拽中变形残留。 ChartSetInteger(0, CHART_MOUSE_SCROLL, true) 重新开启图表鼠标滚轮滚动,交还 MT5 原生缩放权限;若 debugLogging 开启则打印 "Slider drag stopped.",最后 ChartRedraw(0) 强制重绘一次。 验证方式:在 MT5 脚本里加这段,拖完滑块看背景色是否变回灰、滚轮是否恢复可用,即可确认事件分支生效。外汇与贵金属图表高频刷新,此类 GUI 状态机需自测避免卡死交互。
if (moving_state_slider) { moving_state_slider = false; ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); if (debugLogging) Print("Slider drag stopped."); ChartRedraw(class="num">0); } } prev_mouse_state = mouse_state; } }
「画得少,看得清」
这一路把经济日历的滚动条和事件展示重写完,核心就一件事:用第八部分回测过的交互逻辑,把新闻事件在实盘和测试模式里都跑得顺。MT5 里加载 MQL5_NEWS_CALENDAR_PART_9.mq5(166.58 KB)就能直接看到动态导航的效果。 外汇和贵金属本身高波动、高风险,新闻驱动策略只是把事件窗口压扁了,不代表胜率被打包票。你能做的,是把这个仪表盘当底框,按自己的品种和触发条件改参数。 少堆控件、多留通道,盘面反而更容易盯住。