MQL5指标子窗口按钮开发实战
用OBJ_EDIT打造可交互悬停点击面板
一、为什么在指标子窗口做按钮控件
在 MetaTrader 5 中,多数交易者习惯把指标放在主图或独立子窗口。标准指标只负责画线,但 MQL5 允许我们在指标子窗口中创建图形对象,从而构建轻量交互界面。本文示例用按钮面板占据整个子窗口,分三行四列共12个按钮,光标悬停时颜色变暗,点击后颜色更深,并带有工具提示。这种做法不需要 EA,也不会干扰主图分析,是制作自定义面板、快捷开关、信息菜单的低侵入方案。
与 EA 图形面板相比,指标子窗口按钮的优势在于:生命周期随指标依附的图表存在,不占用 Expert Advisors 的订单权限;适合纯展示、参数切换、信号静音等场景。缺点是子窗口高度受用户拖拽限制,且指标无法像 EA 那样直接下单,只能触发内部状态或外部全局变量。
二、基础框架:空子窗口指标骨架
使用 MQL5 向导创建指标,关键属性是指令 #property indicator_separate_window 让指标显示在子窗口,#property indicator_plots 0 表示不绘制任何数据系列。初始化与计算函数留空,仅保留事件与定时器函数框架。此时加载到图表会得到一个空白子窗口,后续所有按钮都在其上叠加。
#property indicator_separate_window
#property indicator_plots 0
int OnInit(){ return(INIT_SUCCEEDED); }
int OnCalculate(const int rates_total,const int prev_calculated,const int begin,const double &price[]){ return(rates_total); }
void OnTimer(){}
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam){}
三、全局常量与二维数组设计
按钮布局用宏 BUTTON_COLUMNS 4 与 BUTTON_ROWS 3 定义。所有按钮相关属性都用二维数组存储:第一维为行、第二维为列。包括对象名、显示文本、宽高、XY坐标、状态与颜色。前缀 prefix 由指标短名加下划线组成,避免多程序对象名冲突。颜色定义了普通、悬停、点击三态,初始状态数组仅 Button 01 为 true,表示默认选中。
#define BUTTON_COLUMNS 4
#define BUTTON_ROWS 3
string button_texts[BUTTON_ROWS][BUTTON_COLUMNS]={{{"Button 01","Button 02","Button 03","Button 04"},{"Button 05","Button 06","Button 07","Button 08"},{"Button 09","Button 10","Button 11","Button 12"}};
bool button_states[BUTTON_ROWS][BUTTON_COLUMNS]={{true,false,false,false},{false,false,false,false},{false,false,false,false}};
四、OnInit 中的初始化流程
在 OnInit 里依次:设置1秒定时器、为所有对象名加前缀、启用鼠标移动事件跟踪、设置子窗口短名、获取子窗口属性、计算按钮颜色/坐标/尺寸、将按钮加入子窗口、重绘。启用 CHART_EVENT_MOUSE_MOVE 是关键,否则 OnChartEvent 收不到鼠标坐标。EventSetTimer 用于后续多程序事件跟踪兜底。
int OnInit(){
EventSetTimer(1);
AddPrefix();
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
IndicatorSetString(INDICATOR_SHORTNAME,subwindow_shortname);
SetSubwindowProperties();
SetButtonColors(); SetButtonCoordinates(); SetButtonSizes();
AddButtonsPanel();
ChartRedraw();
return(INIT_SUCCEEDED);
}
AddPrefix 函数遍历对象名数组加上前缀;SetSubwindowProperties 用 ChartWindowFind 拿到子窗口索引,并用 ChartGetInteger 取宽度与子窗口高度;SetButtonCoordinates 和 SetButtonSizes 按整除分配宽高,并处理最右最下边距补齐,保证铺满。
五、创建按钮与工具提示
CreateButton 是对 OBJ_EDIT 的封装。ObjectCreate 后在子窗口以 (0,0) 时间价格创建,再设文本、字体、颜色、尺寸、距离、只读、居中、工具提示。最后一个参数 tooltip 传入按钮自身文本,悬停即显示。若传 "\n" 则不显示提示。AddButtonsPanel 双重循环调用它,完成12个按钮布局。
void CreateButton(long chart_id,int sub_window,string object_name,string text,long corner,string font,int font_size,color c_font,color c_background,color c_border,int x_size,int y_size,int x_dist,int y_dist,long zorder,bool read_only,string tooltip){
if(ObjectCreate(chart_id,object_name,OBJ_EDIT,subwindow_number,0,0)){
ObjectSetString(chart_id,object_name,OBJPROP_TEXT,text);
ObjectSetInteger(chart_id,object_name,OBJPROP_BGCOLOR,c_background);
ObjectSetInteger(chart_id,object_name,OBJPROP_XSIZE,x_size);
ObjectSetInteger(chart_id,object_name,OBJPROP_YSIZE,y_size);
ObjectSetInteger(chart_id,object_name,OBJPROP_XDISTANCE,x_dist);
ObjectSetInteger(chart_id,object_name,OBJPROP_YDISTANCE,y_dist);
ObjectSetInteger(chart_id,object_name,OBJPROP_READONLY,read_only);
ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
}
}
六、响应图表缩放:CHARTEVENT_CHART_CHANGE
当用户拖动子窗口边界,触发 CHARTEVENT_CHART_CHANGE。我们在 OnChartEvent 中捕获后重新获取子窗口属性、重算坐标与尺寸,调用 UpdateButtonCoordinates 与 ResizeButtons 写回对象属性并 ChartRedraw。这样按钮始终铺满、不扭曲。注意若图表主窗口缩放也会触发该事件,计算基于最新 ChartGetInteger 即可。
if(id==CHARTEVENT_CHART_CHANGE){
SetSubwindowProperties();
SetButtonCoordinates(); SetButtonSizes();
UpdateButtonCoordinates(); ResizeButtons();
ChartRedraw(); return;
}
七、鼠标移动与悬停变色原理
CHARTEVENT_MOUSE_MOVE 的 lparam 为X像素、dparam 为Y像素、sparam 为鼠标键状态(1按下0释放)。用 ChartXYToTimePrice 可获光标所在窗口号。将Y减去 ChartGetInteger(0,CHART_WINDOW_YDISTANCE,subwindow_number) 得到子窗口内相对坐标。若 window==subwindow_number 则禁图表滚动避免误拖,否则恢复。
ChangeButtonColorOnHover 用相对坐标判断落点是否在某个未选中按钮矩形内,是则设 hover_background_color,否则恢复 background_color。每次移动都重绘,实现平滑反馈。初始仅 Button 01 为选中态,故悬停其他按钮才变色。
if(id==CHARTEVENT_MOUSE_MOVE){
int x=(int)lparam, y=(int)dparam, window=WRONG_VALUE;
datetime time; double price;
if(ChartXYToTimePrice(0,x,y,window,time,price)){
chart_y_offset=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,subwindow_number);
y-=chart_y_offset;
if(window==subwindow_number) ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
else ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
ChangeButtonColorOnHover(x,y);
}
ChartRedraw(); return;
}
八、点击切换与状态数组
CHARTEVENT_OBJECT_CLICK 的 sparam 是被点对象名。InitializeButtonStates 校验对象名带前缀且位于子窗口,然后循环将对应按钮状态设 true、其余 false,返回 true。ChangeButtonColorOnClick 依据状态数组上色:选中用 clicked_background_color,否则普通色。这样任意时刻仅一个按钮深亮,类似单选页签。
if(id==CHARTEVENT_OBJECT_CLICK){
if(InitializeButtonStates(sparam)) ChangeButtonColorOnClick();
ChartRedraw(); return;
}
九、退出清理与多程序事件冲突
OnDeinit 在指标删除或重编译时,杀定时器、删对象、恢复图表滚动、关闭鼠标跟踪。关键坑:若图表同时跑多个启用了 CHART_EVENT_MOUSE_MOVE 的程序,其中一个退出会全局关闭跟踪,导致其他程序失灵。原文用 OnTimer 每秒 CheckChartEventMouseMove 检测并重新开启,作为兜底。生产环境建议用全局变量或管理类协调。
void OnDeinit(const int reason){
if(reason==REASON_REMOVE || reason==REASON_RECOMPILE){
EventKillTimer(); DeleteButtons();
ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,false);
ChartRedraw();
}
}
十、扩展方向与对比总结
此面板可扩展为策略开关、周期切换、信息弹窗触发。相比用 OBJ_BUTTON,OBJ_EDIT 只读方案在文字与配色上更统一;相比 EA 面板,它轻量但无交易权限。开发者可结合后续文章的滚动条、多图表控件构建完整终端内 GUI。原文附 TestButtons.mq5 可下载实践。
- 用 OBJ_EDIT 只读模拟按钮,易着色易文本
- 二维数组管理布局,支持自适应缩放
- 鼠标事件需显式启用,相对坐标要减偏移
- 点击用状态数组实现单选视觉
- 退出与多程序事件跟踪需谨慎处理