MQL5经济日历交易指南(第九部分):通过动态滚动条与界面优化提升新闻交互体验·综合运用
📘

MQL5经济日历交易指南(第九部分):通过动态滚动条与界面优化提升新闻交互体验·综合运用

第 3/3 篇

财经日历面板的绘制与滚动条逻辑

这段 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、到底时向下箭头同理,用颜色灰化表达不可点。外汇与贵金属受数据面冲击大,这类面板只作信息辅助,实际波动风险仍高。

MQL5 / C++
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),导致后续绘制失败。

MQL5 / C++
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,拿到滑块相对图表左上角的位置和自身宽度,后面就能算拖拽映射。外汇与贵金属行情受事件冲击大,这类自定义面板帮你快速过滤财经事件,但杠杆交易高风险,任何信号都只是概率倾向。

MQL5 / C++
  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 方便核对。

MQL5 / C++
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 与更新时间戳,再反过来定参数。

MQL5 / C++
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 出状态便于排查。外汇和贵金属受新闻跳空影响大,这类开关能帮你避开高影响时段,但行情仍可能反向,属高风险操作。

MQL5 / C++
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 终端看专家日志就能确认数组增删是否如预期。

MQL5 / C++
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 并进入移除循环。

MQL5 / C++
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 应输出一致长度,否则两个平行数组错位会让后续新闻屏蔽判断失效。

MQL5 / C++
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 的实际触发。

MQL5 / C++
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 再减去滑块高度为止。

MQL5 / C++
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;
}
}
逐行看:第1–2行算出滑块可走的纵向最小和最大像素坐标;第3行用 MathMax/MathMin 把 new_y 夹在这个区间内,防止拖出轨道;第4行把夹好的坐标写回滑块对象的 YDISTANCE。 第5行用 ArraySize(displayableEvents) 减去 VISIBLE_ITEMS 得到最大滚动步数,若列表不够长则 MathMax(0, …) 归零。第6–7行把滑块位置换算成 0~1 的比例再乘 max_scroll,四舍五入得到新滚动位置。 只要新位置不同于旧值,就更新 scroll_pos 并刷新面板数值与按钮颜色;若开了 debugLogging 还会打印当前位置、总步数和滑块 y。最后 ChartRedraw(0) 重绘。 鼠标松开(mouse_state==0)且之前在拖滑块时,代码把 moving_state_slider 置否,恢复滑块背景色与高度,把图表原生滚轮 CHART_MOUSE_SCROLL 重新开启,再重绘一次。 开 MT5 把这段塞进你的面板 EA,设 VISIBLE_ITEMS=10、displayableEvents 长度 50,拖滑块时应在日志看到 Total steps: 40,说明滚动步数映射正确。外汇与贵金属图表挂自定义面板属高风险操作,参数误写可能导致对象错位。

MQL5 / C++
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 超过它才画滚动条。

MQL5 / C++
<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>&amp; lparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span>&amp; dparam, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">string</span>&amp; sparam) {
&nbsp;&nbsp; <span class="keyword">class="type">int</span> mouse_x = (<span class="keyword">class="type">int</span>)lparam;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> mouse_y = (<span class="keyword">class="type">int</span>)dparam;
&nbsp;&nbsp; <span class="keyword">class="type">int</span> mouse_state = (<span class="keyword">class="type">int</span>)sparam;
&nbsp;&nbsp; <span class="keyword">if</span> (id == <span class="macro">CHARTEVENT_OBJECT_CLICK</span>) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;UpdateFilterInfo();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CheckForNewsTrade();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (sparam == CANCEL_BTN) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; isDashboardUpdate = <span class="macro">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; destroy_Dashboard();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (sparam == FILTER_CURR_BTN) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; enableCurrencyFilter = btn_state;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">class="kw">color</span> filter_curr_txt_color = enableCurrencyFilter ? <span class="macro">clrLime</span> : <span class="macro">clrRed</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectSetString</span>(<span class="number">class="num">0</span>,FILTER_CURR_BTN,<span class="macro">OBJPROP_TEXT</span>,filter_curr_text);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>)) filters_changed = <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; update_dashboard_values(curr_filter_selected,imp_filter_selected);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Recalculate scrollbar</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_LEADER);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_UP_REC);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_UP_LABEL);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_DOWN_REC);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_DOWN_LABEL);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectDelete</span>(<span class="number">class="num">0</span>, SCROLL_SLIDER);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; scroll_visible = totalEvents_Filtered &gt; VISIBLE_ITEMS;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (scroll_visible) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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 打开,能在专家日志里核对滑块是否随事件数线性变化,外汇与贵金属事件面板的高频刷新本身带有终端卡顿风险,参数别乱改。

MQL5 / C++
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 完成重绘。外汇与贵金属事件受突发新闻影响大,面板筛选仅作信息辅助,实际波动高风险。

MQL5 / C++
  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() 动态得出,这套逻辑直接决定长事件列表能不能顺手翻。

MQL5 / C++
  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 过滤仅改变信息展示范围,不预示任何方向,参数验证请在模拟环境先跑通。

MQL5 / C++
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 往下推。

MQL5 / C++
      }
            }
            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 的映射是否和按钮文本严格一致。

MQL5 / C++
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。外汇与贵金属事件过滤涉及高风险,实盘前请在策略测试器跑通该分支。

MQL5 / C++
            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 有没有被别的标签挡住。

MQL5 / C++
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 就能看到拖拽全程数值。

MQL5 / C++
         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 状态机需自测避免卡死交互。

MQL5 / C++
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)就能直接看到动态导航的效果。 外汇和贵金属本身高波动、高风险,新闻驱动策略只是把事件窗口压扁了,不代表胜率被打包票。你能做的,是把这个仪表盘当底框,按自己的品种和触发条件改参数。 少堆控件、多留通道,盘面反而更容易盯住。

常见问题

先检查滚动条和面板是否用了同一套坐标基准,生命周期里对象没随图表重绘而重建就会失效,建议在面板初始化时统一创建并绑定鼠标事件。
面板销毁时要主动释放对象并恢复图表默认鼠标响应,否则点击事件会悬空;在销毁函数里清空事件绑定即可正常接管。
可以,小布能直接读取品种页的新闻过滤状态并语音或一键切换,你点一下对应品种页的AI助手就能改条件,不用手动拖面板。
只要滑块只拦截自身区域内的鼠标事件、区域外放行,就不会影响图表原生缩放平移,注意事件返回值别全吞掉。
心跳间隔设在1到2秒较稳,太快要重绘对象拖慢图表,太慢新闻延迟;可用计时器变量控制重绘频率。