MQL5自绘滚动条:指标子窗口控件实战
📜

MQL5自绘滚动条:指标子窗口控件实战

用画布与矩形标签打造可拖拽垂直滚动条

MQL5 编程 难度 · 进阶 2026-01-13 10 分钟阅读
#MQL5#指标开发#滚动条#CCanvas

为什么要在指标子窗口自己做滚动条

MetaTrader 5 的指标子窗口默认不能像常规控件那样直接放一个系统滚动条。当我们需要在有限高度的子窗口里展示远超可视区域的内容(例如文中所用的 71 条交易品种属性)时,就必须自己实现滚动机制。前文用 OBJ_LABEL 堆出列表,对象数量多、性能差;本文改用 CCanvas 画布,只用一个 OBJ_BITMAP_LABEL 把文字画在位图上,再用两个 OBJ_RECTANGLE_LABEL 做滚动条背景与滚动框,对象总数降到三个,极大减轻终端负担。

这种方案的本质是:用画布模拟“虚拟长列表”,用滚动框的位置百分比决定画布从哪一行开始截取其可视部分。它不依赖任何外部库,纯 MQL5 标准库即可完成,适合需要轻量、可控 GUI 的自定义指标。

✦ 前置阅读
本文大量复用 OnChartEvent 事件处理与子窗口属性获取逻辑,建议先读同作者《指标子窗口控件 - 按钮》一文,文中 SetSubwindowProperties 等函数直接沿用。

核心概念:画布 CCanvas 与图形对象分工

CCanvas 是 MQL5 标准库提供的位图绘制类。我们包含 <Canvas\Canvas.mqh> 后声明全局 canvas 对象。它的 CreateBitmapLabel 方法在子窗口创建一张可绘制的位图标签;Resize 调整尺寸;Erase 用背景色清屏;TextOut 在指定坐标写文字;Update 把内存位图推到屏幕。字体用 FontSet 设置,文字高度用 TextHeight 取。

滚动条本身不用画布,而是两个矩形标签:背景用 scrollbar_background_name,滚动框用 scrollbar_thumb_name。二者都是 OBJ_RECTANGLE_LABEL,通过 OBJPROP_XDISTANCE / YDISTANCE / XSIZE / YSIZE 控制位置和大小,OBJPROP_BGCOLOR 和 OBJPROP_COLOR 控制颜色。这样滚动框可以独立响应鼠标,不影响画布内容。

初始化:OnInit 里铺好道路

在 OnInit 中,首先要 ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true) 打开鼠标移动事件,否则悬停和拖拽都收不到。然后设置指标短名、给属性数组 ArrayResize 到 LIST_SIZE(71)。SetSubwindowProperties 拿到子窗口编号与高度。canvas.FontSet 设定字体与字号,text_height 通过 TextHeight("A")-1 得到,list_height 就是 text_height*LIST_SIZE,代表完整列表像素高。

随后 AddCanvas 建画布,AdjustScrollbarBackground 与 AdjustScrollbarThumb 建滚动条,最后 ShowSymbolInfo() 无参调用显示第一屏。注意:若子窗口高度大于列表高,滚动框高度直接等于子窗口高减一,此时无需滚动。

int OnInit()
  {
   ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
   IndicatorSetString(INDICATOR_SHORTNAME,subwindow_shortname);
   ArrayResize(symbol_property_colors,LIST_SIZE);
   ArrayResize(symbol_property_values,LIST_SIZE);
   SetSubwindowProperties();
   canvas.FontSet(font_name,font_size,FW_NORMAL);
   text_height=canvas.TextHeight("A")-1;
   list_height=text_height*LIST_SIZE;
   AddCanvas();
   AdjustScrollbarBackground();
   AdjustScrollbarThumb();
   ShowSymbolInfo();
   ChartRedraw();
   return(INIT_SUCCEEDED);
  }

画布显示列表:ShowSymbolInfo 的工作流

ShowSymbolInfo 接收 current_thumb_position(百分比,默认0)。它先循环把 thumb_position 累加到大于等于传入值,得出起始行 line_number。接着 InitializePropertyArrays(line_number) 从该行开始填值和颜色数组。然后 canvas.Erase 清屏,循环用 TextOut 画属性名(右对齐)和值(左对齐),y_distance 每次加 text_height;一旦画出行数撑出子窗口高就 break。最后 canvas.Update 刷新。

InitializePropertyArrays 内部调用 GetStringSymbolInfoByIndex 和 GetColorSymbolInfoByIndex。前者用 SymbolInfoInteger / SymbolInfoDouble 取真实数据,并把枚举转成字符串(例如 TradeCalcModeToString)。后者按索引给不同属性上色,例如点差浮动为金色、非浮动为红。辅助函数 IsExpirationTypeAllowed 用位与判断模式是否可用,避免显示不存在的到期方式。

void ShowSymbolInfo(double current_thumb_position=0.0)
  {
   int    list_lines=0;
   double thumb_position=0.0;
   int    y_distance=0;
   int    line_number=0;
   for(int i=0; i<LIST_SIZE; i++)
     {
      if(thumb_position>=current_thumb_position) break;
      thumb_position+=line_size;
      line_number++;
     }
   InitializePropertyArrays(line_number);
   canvas.Erase(canvas_background_color);
   for(int i=line_number; i<LIST_SIZE; i++)
     {
      canvas.TextOut(655,y_distance,symbol_propety_names[i]+" :",ColorToARGB(clrWhite),TA_RIGHT|TA_TOP);
      canvas.TextOut(665,y_distance,symbol_property_values[i],ColorToARGB(symbol_property_colors[i]),TA_LEFT|TA_TOP);
      y_distance+=text_height;
      list_lines++;
      if(list_lines*text_height>subwindow_height) break;
     }
   canvas.Update();
  }

滚动条几何:高度计算与边界约束

CalculateScrollbarThumbHeight 决定滑块高。若 subwindow_height>=list_height,滑块占满;否则按可视比例算:height_temp = 子窗口高 - (子窗口高/100)*(100 - (子窗口高/列表高)*100),并保证最小为子窗口高的 25%。AdjustScrollbarThumb 据此设 OBJPROP_YSIZE,并把 X 推到 chart_width - thumb_width;若底部超出则钉在底部。AdjustScrollbarBackground 则让背景槽和子窗口同高、同 X。

CreateRectangleLable 是通用建矩形标签函数,设了 BORDER_FLAT、无 tooltip("\n")等。滚动框和背景都设为不可选(selectable=false),避免误选中;背景 is_on_background 可false,因我们手动管理层级。

void CalculateScrollbarThumbHeight()
  {
   if(subwindow_height>=list_height)
      scrollbar_thumb_height=subwindow_height-1;
   else
     {
      double height_temp=0.0;
      height_temp=subwindow_height-(((double)subwindow_height/100)*(100-((double)subwindow_height/list_height)*100));
      if(height_temp/subwindow_height<0.25)
         height_temp=subwindow_height/4;
      scrollbar_thumb_height=(int)height_temp;
     }
  }

交互逻辑:悬停、点击与拖拽

在 OnChartEvent 的 CHARTEVENT_MOUSE_MOVE 里,先 SetSubwindowProperties 刷新尺寸,CheckMouseButtonState 从 sparam 读"1"/"0"知左右键。ChartXYToTimePrice 拿到窗口号,若等于 subwindow_number 说明在子窗内,ChartYToSubwindowY 把全局 y 改成相对子窗的 y。SetScrollbarThumbBoundaries 读滑块当前 x1/y1/x2/y2。

ChangeScrollbarThumbColorOnHover 判断光标是否在滑块矩形内,是则设 hover 色,否则若没按鼠标就恢复标准色。SetScrollbarThumbState 在按下且位于滑块内时置 scrollbar_thumb_clicked=true,把控制权交给滑块。MoveThumb 是拖拽核心:记录按下时 y 为 fix_point 和偏移,超过阈值 1 像素就计算 new_y_point,调用 UpdateListAndScrollbarThumb 重设滑块 Y 并 ShowSymbolInfo(ThumbYCoordinateToPercent(new_point)) 刷新列表。

⚠ 原文笔误
代码23(中文版)悬停判断误用 y<scrollbar_thumb_x2(应为 y<scrollbar_thumb_y2)。若照抄会导致横向坐标误当纵向边界,悬停变色区域错误。请以英文逻辑 y<scrollbar_thumb_y2 为准。
void MoveThumb(int y)
  {
   int threshold=1;
   int new_y_point=0;
   if(mouse_button_state)
     {
      SetScrollbarThumbColor(scrollbar_thumb_color_on_click);
      if(scrollbar_fix_point==0) scrollbar_fix_point=y;
      if(scrollbar_fix_point_y_offest==0) scrollbar_fix_point_y_offest=scrollbar_thumb_y1-scrollbar_fix_point;
     }
   if(y-scrollbar_fix_point>=threshold)
     {
      if(scrollbar_thumb_y1+scrollbar_thumb_height+threshold<subwindow_height)
         new_y_point=y+scrollbar_fix_point_y_offest;
      else { scrollbar_fix_point_y_offest=0; new_y_point=int(subwindow_height-scrollbar_thumb_height)-1; }
      UpdateListAndScrollbarThumb(new_y_point);
      return;
     }
   if(y-scrollbar_fix_point<=-(threshold))
     {
      if(y-fabs(scrollbar_fix_point_y_offest)>=0) new_y_point=y-fabs(scrollbar_fix_point_y_offest);
      else { new_y_point=0; scrollbar_fix_point_y_offest=0; }
      UpdateListAndScrollbarThumb(new_y_point);
      return;
     }
  }

事件处理与资源清理

OnChartEvent 中 CHARTEVENT_CLICK 用于丢焦点:ZeroScrollbarThumbVariables 并重绘。鼠标移动分支如上所述,在子窗外要 ChartSetInteger(0,CHART_MOUSE_SCROLL,true) 恢复价格图滚动,避免自己控件吃掉主图滚轮。OnDeinit 按 reason 删滚动条、画布,重开鼠标滚动与事件,防残留对象。OnCalculate 可加几行刷新实时属性(如买价卖价),让列表随行情动。

与标准库 CScroll/CListView 对比:本文手搓方案灵活、依赖少,但需自己算几何与事件;若用 CListView 则自带滚动但对象多、定制难。初学者可先模仿本文,再视需求迁移到标准库控件。

void OnDeinit(const int reason)
  {
   if(reason==REASON_REMOVE || reason==REASON_CHARTCHANGE ||
      reason==REASON_RECOMPILE || reason==REASON_CHARTCLOSE || reason==REASON_CLOSE)
     {
      DeleteScrollbar();
      DeleteCanvas();
      ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
      ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,false);
      ChartRedraw();
     }
  }

常见坑与调试建议

✦ 性能提示
列表行数多时,只在滑块移动超过阈值才重绘画布;平时鼠标移动只改颜色不调 ShowSymbolInfo,可大幅降低 CPU。

常见问题

用 CCanvas 的 CreateBitmapLabel 建一个位图标签画全部文本,再用两个 OBJ_RECTANGLE_LABEL 做背景槽和滚动框。滚动框 Y 坐标百分比传给显示函数决定从哪行截屏,参考本文 ShowSymbolInfo 与 MoveThumb 实现。
每次绘制后必须调用 canvas.Erase(背景色) 清屏,TextOut 写内容,最后 canvas.Update() 推到屏幕。若只 TextOut 不 Update,屏幕位图不会变。
检查悬停判断边界是否写成 y<scrollbar_thumb_x2(错),应为 y<scrollbar_thumb_y2;并确认 CHART_EVENT_MOUSE_MOVE 已开,且 ChartYToSubwindowY 正确换算了相对坐标。
在 OnChartEvent 判定光标位于子窗口内时 ChartSetInteger(0,CHART_MOUSE_SCROLL,false),离开子窗口或鼠标释放时设回 true,避免冲突。
手搓方案对象少、定制自由,适合学原理和轻量需求;CListView 自带滚动但对象多、改样式难。新手可先仿本文,再按项目选标准库。