MQL5自绘滚动条:指标子窗口控件实战
用画布与矩形标签打造可拖拽垂直滚动条
为什么要在指标子窗口自己做滚动条
MetaTrader 5 的指标子窗口默认不能像常规控件那样直接放一个系统滚动条。当我们需要在有限高度的子窗口里展示远超可视区域的内容(例如文中所用的 71 条交易品种属性)时,就必须自己实现滚动机制。前文用 OBJ_LABEL 堆出列表,对象数量多、性能差;本文改用 CCanvas 画布,只用一个 OBJ_BITMAP_LABEL 把文字画在位图上,再用两个 OBJ_RECTANGLE_LABEL 做滚动条背景与滚动框,对象总数降到三个,极大减轻终端负担。
这种方案的本质是:用画布模拟“虚拟长列表”,用滚动框的位置百分比决定画布从哪一行开始截取其可视部分。它不依赖任何外部库,纯 MQL5 标准库即可完成,适合需要轻量、可控 GUI 的自定义指标。
核心概念:画布 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 控制颜色。这样滚动框可以独立响应鼠标,不影响画布内容。
- OBJ_BITMAP_LABEL:承载全部文本的唯一画布对象
- OBJ_RECTANGLE_LABEL(背景):静止的滚动槽
- OBJ_RECTANGLE_LABEL(滚动框):可拖拽的滑块
初始化: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)) 刷新列表。
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();
}
}
常见坑与调试建议
- 忘记开 CHART_EVENT_MOUSE_MOVE,导致拖拽无响应
- ChartYToSubwindowY 未减偏移,y 坐标错乱使滑块跑飞
- 悬停判断 y 边界写成 x2,变色区域错误(见上文警告)
- 缩轮子窗口后未 ResizeCanvas,画布留黑边
- OBJ_RECTANGLE_LABEL 设了 selectable=true,鼠标点中后抢走事件
- OnDeinit 不清理,切换周期后旧对象堆积