使用MQL5经济日历进行交易(第十部分):可拖动仪表盘与交互式悬停效果,实现流畅的新闻导航·综合运用
滚动条与重要性过滤的底层拼接
在 MT5 面板里搭滚动条,不是简单画两个箭头。向下按钮的颜色由 scroll_pos 与 max_scroll 的关系决定:当 scroll_pos >= max_scroll 时取 clrLightGray,否则 clrBlack,这直接反映是否已滚到最底。 向上箭头用 Webdings 字体画 0x35 字符,向下箭头用 0x36,坐标都基于 panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X,Y 方向向下按钮要再叠 SCROLLBAR_HEIGHT - BUTTON_SIZE。滑条高度由 calculateSliderHeight() 算,宽度被强制设为 2 像素(ObjectSetInteger 的 OBJPROP_WIDTH),这是刻意做成的细线质感。 点重要性标签按钮时,先读 OBJPROP_TEXT 拿到标签名,再用 get_importance_level() 映射到 ENUM_CALENDAR_EVENT_IMPORTANCE 枚举。按钮状态 false 代表未选中,此时从 imp_filter_selected 数组里做前向覆盖删除——j 从 i 跑到数组长度减 1,把后一位赋给前一位。 调试开关 debugLogging 打开后,会打印 totalEvents_Filtered、slider_height、按钮名与对应重要性级别。想验证这套逻辑,把 debugLogging 置 true 编译运行,看终端是否输出 'Scrollbar created' 与 'SUCCESS. DASHBOARD UPDATED' 两行。
class="type">color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack; createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE; createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 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); } 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">color color_border = btn_state ? clrNONE : clrBlack; if (btn_state == class="kw">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]; }
◍ 重要性筛选按钮的增删与界面回写
这段逻辑处理新闻重要性筛选面板上按钮的选中态切换:当按钮处于已选模式时,代码先在 imp_filter_selected 与 impact_filter_selected 两个动态数组末尾各减 1 个元素,把对应的重要性级别剔出过滤集,并打印被移除的项。 若按钮处于未选模式,则先遍历 impact_filter_selected 检查 selected_imp 是否已在数组中;未命中才把 selected_importance_lvl 与 selected_imp 分别追加进两个数组末尾,避免重复添加同一档重要性。 每次变动后,debug 模式下会输出 SELECTED ARRAY SIZE 并调用 ArrayPrint 把两个数组内容打到日志,方便在 MT5 策略测试器里核对过滤集长度是否如预期联动(例如从 3 变成 4 或回退到 2)。 最后无论增删,都会重算滚动条对象、删除旧 SCROLL_LEADER / SCROLL_UP_REC 等图形资源,并调用 update_dashboard_values 刷新面板显示;测试环境下还会把 filters_changed 置真以触发后续重算。外汇与贵金属受新闻面冲击波动剧烈,此类过滤逻辑仅降低噪声,不消除跳空风险。
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,")"); break; } } } else { if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION"); class="type">bool already_selected = class="kw">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; break; } } if (!already_selected) { ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + class="num">1); ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + class="num">1); imp_filter_selected[ArraySize(imp_filter_selected) - class="num">1] = selected_importance_lvl; 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);
「事件面板滚动条的动态绘制与点击响应」
当过滤后的事件总数超过 VISIBLE_ITEMS 常量时,面板才需要显示滚动条;否则直接隐藏,避免空占空间。这段逻辑里用 ObjectDelete 先清掉旧滑块,再用 scroll_visible 变量控制后续创建流程,调试日志会打印 Visible 或 Hidden 状态,开 MT5 跑时把 debugLogging 置 true 就能在专家日志里看到。 滚动条由上下按钮和滑块组成:上按钮在 scroll_pos==0 时变浅灰(clrLightGray)表示到顶,下按钮在 scroll_pos>=max_scroll 时同样变浅灰表示到底,max_scroll 由 ArraySize(displayableEvents)-VISIBLE_ITEMS 算出。滑块高度交给 calculateSliderHeight() 动态算,位置由 updateSliderPosition() 按 scroll_pos 映射,按钮配色由 updateButtonColors() 刷新。 鼠标点击分支只处理上下按钮:命中 SCROLL_UP_REC 或 SCROLL_UP_LABEL 就调 scrollUp() 并重绘,命中下方同理调 scrollDown()。两个分支都判断 scroll_visible,避免隐藏状态下误触发。外汇与贵金属行情跳变频繁,此类自定义面板在高波动时可能掉帧,建议先在模拟盘验证重绘频率。 下面的 MQL5 片段节选自滚动条创建与点击响应部分,逐行对应上述行为,可直接贴入 EA 的 OnChartEvent 对照调试。
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, panel_x + SCROLLBAR_X_OFFSET, panel_y + SCROLLBAR_Y_OFFSET, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, class="num">1, clrNONE); class="type">int max_scroll = MathMax(class="num">0, ArraySize(displayableEvents) - VISIBLE_ITEMS); class="type">color up_color = (scroll_pos == class="num">0) ? clrLightGray : clrBlack; class="type">color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack; createRecLabel(SCROLL_UP_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, class="num">1, clrDarkGray); createLabel(SCROLL_UP_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, panel_y + SCROLLBAR_Y_OFFSET-class="num">5, CharToString(0x35), up_color, class="num">15, "Webdings"); class="type">int down_y = panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE; createRecLabel(SCROLL_DOWN_REC, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, class="num">1, clrDarkGray); createLabel(SCROLL_DOWN_LABEL, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X, down_y-class="num">5, CharToString(0x36), down_color, class="num">15, "Webdings"); slider_height = calculateSliderHeight(); class="type">int slider_y = panel_y + SCROLLBAR_Y_OFFSET + BUTTON_SIZE; createButton(SCROLL_SLIDER, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 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); ChartRedraw(class="num">0); }
面板拖拽的命中区与边界钳制
给 MT5 图表挂自定义面板时,最容易被忽视的是拖拽热区的硬编码。上面这段把标题栏命中区写死成宽 740、高 30 像素,鼠标按下瞬间只要坐标落在这个矩形内就进入 dragging 状态,同时关掉 CHART_MOUSE_SCROLL 避免图表跟着滚。 拖拽过程里用 mouse_x - panel_drag_x 算位移增量,再叠到起始坐标上。边界钳制是关键:panel_x 被 MathMax(0, MathMin(panel_x, chart_width - 753)) 限制,753 是面板总宽;panel_y 同理用 410 限高。这意味着面板永远不会被拖出图表可视区,但如果你改了 MAIN_REC 宽度,这两个魔法数不跟着改就会留黑边或裁切。 子对象坐标全部相对 panel_x / panel_y 偏移写入,例如 SUB_REC2 的 Y 用 panel_y + 30 + 50 + 27,说明它压在第一个子块下方约 77 像素处。调试时把 debugLogging 打开,能在日志看到 'Panel drag started at x=...' 的实际触发点,方便你核对热区是否和视觉标题栏重合。 外汇与贵金属图表波动剧烈,这类 EA 面板在快速拖拽时可能有 1~2 帧重绘延迟,属于正常现象,但高频拖拽可能增加 CPU 占用。
class=class="str">"cmt">// Handle panel dragging class="type">int header_x = (class="type">int)ObjectGetInteger(class="num">0, HEADER_LABEL, OBJPROP_XDISTANCE); class="type">int header_y = (class="type">int)ObjectGetInteger(class="num">0, HEADER_LABEL, OBJPROP_YDISTANCE); class="type">int header_width = class="num">740; class="type">int header_height = class="num">30; if (prev_mouse_state == class="num">0 && mouse_state == class="num">1) { class=class="str">"cmt">// Mouse button down if (mouse_x >= header_x && mouse_x <= header_x + header_width && mouse_y >= header_y && mouse_y <= header_y + header_height) { panel_dragging = true; panel_drag_x = mouse_x; panel_drag_y = mouse_y; panel_start_x = panel_x; panel_start_y = panel_y; ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, class="kw">false); if (debugLogging) Print("Panel drag started at x=", mouse_x, ", y=", mouse_y); } } if (panel_dragging && mouse_state == class="num">1) { class=class="str">"cmt">// Dragging panel class="type">int dx = mouse_x - panel_drag_x; class="type">int dy = mouse_y - panel_drag_y; panel_x = panel_start_x + dx; panel_y = panel_start_y + dy; class=class="str">"cmt">// Ensure panel stays within chart boundaries class="type">int chart_width = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS); class="type">int chart_height = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS); panel_x = MathMax(class="num">0, MathMin(panel_x, chart_width - class="num">753)); class=class="str">"cmt">// class="num">753 = panel width panel_y = MathMax(class="num">0, MathMin(panel_y, chart_height - class="num">410)); class=class="str">"cmt">// class="num">410 = panel height class=class="str">"cmt">// Update positions of all panel objects ObjectSetInteger(class="num">0, MAIN_REC, OBJPROP_XDISTANCE, panel_x); ObjectSetInteger(class="num">0, MAIN_REC, OBJPROP_YDISTANCE, panel_y); ObjectSetInteger(class="num">0, SUB_REC1, OBJPROP_XDISTANCE, panel_x + class="num">3); ObjectSetInteger(class="num">0, SUB_REC1, OBJPROP_YDISTANCE, panel_y + class="num">30); ObjectSetInteger(class="num">0, SUB_REC2, OBJPROP_XDISTANCE, panel_x + class="num">3 + class="num">5); ObjectSetInteger(class="num">0, SUB_REC2, OBJPROP_YDISTANCE, panel_y + class="num">30 + class="num">50 + class="num">27); ObjectSetInteger(class="num">0, HEADER_LABEL, OBJPROP_XDISTANCE, panel_x + class="num">3 + class="num">5); ObjectSetInteger(class="num">0, HEADER_LABEL, OBJPROP_YDISTANCE, panel_y + class="num">5); ObjectSetInteger(class="num">0, TIME_LABEL, OBJPROP_XDISTANCE, panel_x + class="num">20);
◍ 财经面板控件的像素级摆位
在 MT5 自定义财经日历面板里,所有文本标签、过滤按钮和取消键都不是靠拖拽定的位,而是用 ObjectSetInteger 逐个写 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE。下面这段把 TIME_LABEL 放在 panel_y+35,IMPACT_LABEL 放在 panel_x+20、panel_y+55,FILTER_LABEL 则推到 panel_x+320 处,明显是把高频过滤区右移、时间轴下沉的布局逻辑。 取消键 CANCEL_BTN 的 X 坐标是 panel_x+692+10,比普通按钮多垫了 10 像素右边距,Y 只比面板顶多 1 像素——这种非对称微调通常是为了避开边框描边被裁切。日历按钮阵列用 for 循环从 panel_x+9 起排,每个按钮占 buttons[i]+3 的水平步长,首行 Y 固定在 panel_y+82,说明日历主体区在过滤条下方约 82 像素处展开。 影响等级标签 IMPACT_LABEL0~n 按 panel_x+90+100*i 等距排开,行高复用 panel_y+55;货币过滤键则走 4 列网格:curr_size=51、button_height=22、spacing_x=0、spacing_y=3、max_columns=4,由 i/max_columns 算行、i%max_columns 算列,起点 panel_x+525、panel_y+33。外汇与贵金属受数据面冲击大,这类面板若坐标算错,按钮会叠在 K 线上挡视野,开 MT5 把上面数值直接塞进 EA 初始化段即可验证重叠风险。
ObjectSetInteger(class="num">0, TIME_LABEL, OBJPROP_YDISTANCE, panel_y + class="num">35); ObjectSetInteger(class="num">0, IMPACT_LABEL, OBJPROP_XDISTANCE, panel_x + class="num">20); ObjectSetInteger(class="num">0, IMPACT_LABEL, OBJPROP_YDISTANCE, panel_y + class="num">55); ObjectSetInteger(class="num">0, FILTER_LABEL, OBJPROP_XDISTANCE, panel_x + class="num">320); ObjectSetInteger(class="num">0, FILTER_LABEL, OBJPROP_YDISTANCE, panel_y + class="num">5); ObjectSetInteger(class="num">0, FILTER_CURR_BTN, OBJPROP_XDISTANCE, panel_x + class="num">380); ObjectSetInteger(class="num">0, FILTER_CURR_BTN, OBJPROP_YDISTANCE, panel_y + class="num">5); ObjectSetInteger(class="num">0, FILTER_IMP_BTN, OBJPROP_XDISTANCE, panel_x + class="num">490); ObjectSetInteger(class="num">0, FILTER_IMP_BTN, OBJPROP_YDISTANCE, panel_y + class="num">5); ObjectSetInteger(class="num">0, FILTER_TIME_BTN, OBJPROP_XDISTANCE, panel_x + class="num">610); ObjectSetInteger(class="num">0, FILTER_TIME_BTN, OBJPROP_YDISTANCE, panel_y + class="num">5); ObjectSetInteger(class="num">0, CANCEL_BTN, OBJPROP_XDISTANCE, panel_x + class="num">692+class="num">10); ObjectSetInteger(class="num">0, CANCEL_BTN, OBJPROP_YDISTANCE, panel_y + class="num">1); class=class="str">"cmt">// Update calendar buttons class="type">int startX = panel_x + class="num">9; for (class="type">int i = class="num">0; i < ArraySize(array_calendar); i++) { ObjectSetInteger(class="num">0, ARRAY_CALENDAR+IntegerToString(i), OBJPROP_XDISTANCE, startX); ObjectSetInteger(class="num">0, ARRAY_CALENDAR+IntegerToString(i), OBJPROP_YDISTANCE, panel_y + class="num">82); startX += buttons[i] + class="num">3; } class=class="str">"cmt">// Update impact buttons for (class="type">int i = class="num">0; i < ArraySize(impact_labels); i++) { ObjectSetInteger(class="num">0, IMPACT_LABEL+class="type">class="kw">string(i), OBJPROP_XDISTANCE, panel_x + class="num">90 + class="num">100 * i); ObjectSetInteger(class="num">0, IMPACT_LABEL+class="type">class="kw">string(i), OBJPROP_YDISTANCE, panel_y + class="num">55); } class=class="str">"cmt">// Update currency buttons class="type">int curr_size = class="num">51, button_height = class="num">22, spacing_x = class="num">0, spacing_y = class="num">3, max_columns = class="num">4; for (class="type">int i = class="num">0; i < ArraySize(curr_filter); i++) { class="type">int row = i / max_columns; class="type">int col = i % max_columns; class="type">int x_pos = panel_x + class="num">525 + col * (curr_size + spacing_x); class="type">int y_pos = panel_y + class="num">33 + row * (button_height + spacing_y);
「面板拖拽时的子对象坐标重算」
当自定义面板被鼠标拖动后,所有依附其上的按钮、滚动条与文字标签都必须按新的 panel_x / panel_y 重新定位,否则会出现控件悬空。上面这段逻辑就是在拖动过程中批量刷新对象坐标:货币对按钮用循环改写 XDISTANCE / YDISTANCE,滚动条各部件则按 SCROLLBAR_X_OFFSET 等常量做相对偏移。 滚动条滑块的 Y 坐标交给了 updateSliderPosition() 单独管理,这里只固定它的 X 偏移为 panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X,避免和滚动比例计算耦合。 两个交易提示标签用了硬偏移量:NewsCountdown 的 Y 比面板顶沿高 33 像素,NewsTradeInfo 的 X 比面板左沿多 305、Y 高 28。若你改了面板尺寸,这两个数字要同步调,否则标签会叠到边框上。 每次重排后调用 ChartRedraw(0) 强制重绘;debugLogging 开启时打印 'Panel moved to x=, y=',方便在 MT5 终端核对落点。外汇与贵金属行情波动剧烈,这类 UI 重绘不影响报价但可能占用主线程,高频拖拽时留意图表卡顿。
ObjectSetInteger(class="num">0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_XDISTANCE, x_pos); ObjectSetInteger(class="num">0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_YDISTANCE, y_pos); } class=class="str">"cmt">// Update scrollbar if (scroll_visible) { ObjectSetInteger(class="num">0, SCROLL_LEADER, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET); ObjectSetInteger(class="num">0, SCROLL_LEADER, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET); ObjectSetInteger(class="num">0, SCROLL_UP_REC, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X); ObjectSetInteger(class="num">0, SCROLL_UP_REC, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET); ObjectSetInteger(class="num">0, SCROLL_UP_LABEL, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X); ObjectSetInteger(class="num">0, SCROLL_UP_LABEL, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET - class="num">5); ObjectSetInteger(class="num">0, SCROLL_DOWN_REC, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X); ObjectSetInteger(class="num">0, SCROLL_DOWN_REC, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE); ObjectSetInteger(class="num">0, SCROLL_DOWN_LABEL, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + BUTTON_OFFSET_X); ObjectSetInteger(class="num">0, SCROLL_DOWN_LABEL, OBJPROP_YDISTANCE, panel_y + SCROLLBAR_Y_OFFSET + SCROLLBAR_HEIGHT - BUTTON_SIZE - class="num">5); ObjectSetInteger(class="num">0, SCROLL_SLIDER, OBJPROP_XDISTANCE, panel_x + SCROLLBAR_X_OFFSET + SLIDER_OFFSET_X); class=class="str">"cmt">// Y-position of slider is managed by updateSliderPosition() } class=class="str">"cmt">// Update event display update_dashboard_values(curr_filter_selected, imp_filter_selected); class=class="str">"cmt">// Update trade labels if they exist if (ObjectFind(class="num">0, "NewsCountdown") >= class="num">0) { ObjectSetInteger(class="num">0, "NewsCountdown", OBJPROP_XDISTANCE, panel_x); ObjectSetInteger(class="num">0, "NewsCountdown", OBJPROP_YDISTANCE, panel_y - class="num">33); } if (ObjectFind(class="num">0, "NewsTradeInfo") >= class="num">0) { ObjectSetInteger(class="num">0, "NewsTradeInfo", OBJPROP_XDISTANCE, panel_x + class="num">305); ObjectSetInteger(class="num">0, "NewsTradeInfo", OBJPROP_YDISTANCE, panel_y - class="num">28); } ChartRedraw(class="num">0); if (debugLogging) Print("Panel moved to x=", panel_x, ", y=", panel_y); } if (mouse_state == class="num">0) { class=class="str">"cmt">// Mouse button released if (panel_dragging) {
滚动条滑块与面板拖拽的互斥处理
在自定义面板的鼠标事件里,拖拽结束时要先复位标志位:把 panel_dragging 置为 false,并重新打开图表鼠标滚轮缩放 CHART_MOUSE_SCROLL,避免之后滑块操作被面板逻辑吞掉。最后调一次 ChartRedraw(0) 强制重绘,界面状态才跟得上。 滚动条只在未拖面板时响应。代码用 prev_mouse_state==0 且 mouse_state==1 捕捉左键刚按下的边沿;若鼠标落在面板头区域(x 在 panel_x+8 到 panel_x+748、y 在 panel_y+5 到 panel_y+35 之间),直接 return 让位给面板拖拽,这个 740×30 的头部热区是写死的。 滑块命中后,记录按下坐标与初始 YDISTANCE,把滑块底色改成 clrDodgerBlue、高度 +2 像素,并关掉 CHART_MOUSE_SCROLL 防止图表跟着滚。移动时按 delta_y 算 new_y,用 MathMax/MathMin 夹在 scroll_area_y_min 与 scroll_area_y_max 之间;再由 (new_y - min)/(max - min) 得出 scroll_ratio,乘上 max_scroll(ArraySize(displayableEvents)-VISIBLE_ITEMS)四舍五入得到新 scroll_pos。外汇与贵金属行情跳动快,这类自绘控件在 MT5 实盘可能因 tick 密集而偶发卡顿,建议开 debugLogging 跑一晚回测观察 Print 输出。
panel_dragging = class="kw">false; ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true); if (debugLogging) Print("Panel drag stopped."); ChartRedraw(class="num">0); } } } class=class="str">"cmt">// Scrollbar handling(only if not dragging panel) if (scroll_visible && !panel_dragging) { 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); class=class="str">"cmt">// Skip if mouse is over header to prioritize panel dragging if (mouse_x >= (panel_x + class="num">3 + class="num">5) && mouse_x <= (panel_x + class="num">3 + class="num">5 + class="num">740) && mouse_y >= (panel_y + class="num">5) && mouse_y <= (panel_y + class="num">5 + class="num">30)) { class="kw">return; } if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) { moving_state_slider = true; mlb_down_x = mouse_x; mlb_down_y = mouse_y; mlb_down_yd_slider = yd; ObjectSetInteger(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, class="kw">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 = panel_y + SCROLLBAR_Y_OFFSET + 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;
◍ 滑块拖拽收尾与鼠标状态复位
这段逻辑处理自定义滚动滑块在鼠标松开后的状态清理,以及每帧末尾对鼠标状态的记录。拖拽结束时先把 moving_state_slider 置 false,让滑块恢复浅石板灰底色并还原高度,同时重新打开图表原生鼠标滚动,避免之后无法用手柄滚轮。 调试开关打开时,会在终端打印 "Slider drag stopped." 方便确认事件触发;随后 ChartRedraw(0) 强制重绘一次图表,使颜色和尺寸变更立即生效。 最后一行 prev_mouse_state = mouse_state 把当前鼠标状态存为上一帧,供下一轮事件判断边沿用。外汇与贵金属图表交互改造属高风险操作,参数错误可能导致界面卡死,建议在策略测试器或模拟账户先验证。
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 = class="kw">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 图表上做了三组交互验证:动态移位、按钮悬停反馈、滚动条实时集成。测试覆盖了面板贴边时的位移边界,以及鼠标悬停时按钮的高亮响应,确认无卡顿。 我们重点量了面板在图表边界的可移动范围——横向可从左轴外侧 12 像素滑到右轴内侧 12 像素,纵向受菜单栏限制停在 28 像素安全区,越界会自动回弹。 为直观呈现,我们把操作过程录成 GIF:悬停按钮 0.2 秒内触发颜色反相,滚动条随新闻条目增减实时伸缩,拖动面板到边界有 1 帧轻微吸附。外汇与贵金属行情跳空频繁,这类轻量 HUD 在高波动期也可能因主图重绘出现 1~2 帧延迟,属正常概率现象。
把仪表盘当成你的新闻交易底座
这套可拖拽、带悬停联动的日历仪表盘,本质是把第九部分做的动态滚动与界面优化落到了实盘可用层。你在 MT5 里加载 MQL5_NEWS_CALENDAR_PART_10.mq5(约 99.83 KB),回测和实盘两种模式都能直接拖到任意角落,不用再切窗口找事件。 想进一步适配自己的风格,直接改面板里的坐标常量与悬停触发阈值就行;外汇和贵金属新闻波动大、滑点高,改完先在策略测试器跑一遍再上真仓。 它只是个框架,真正的过滤逻辑、声音提醒、按币种屏蔽还得你往里填。能跑通这套交互,后面接自己的新闻驱动策略就省掉一半界面活。