MQL5指标子窗口按钮开发实战
🔘

MQL5指标子窗口按钮开发实战

用OBJ_EDIT打造可交互悬停点击面板

MQL5 编程 难度 · 进阶 2026-06-10 8 分钟阅读
#MQL5#指标子窗口#GUI按钮#事件处理

一、为什么在指标子窗口做按钮控件

在 MetaTrader 5 中,多数交易者习惯把指标放在主图或独立子窗口。标准指标只负责画线,但 MQL5 允许我们在指标子窗口中创建图形对象,从而构建轻量交互界面。本文示例用按钮面板占据整个子窗口,分三行四列共12个按钮,光标悬停时颜色变暗,点击后颜色更深,并带有工具提示。这种做法不需要 EA,也不会干扰主图分析,是制作自定义面板、快捷开关、信息菜单的低侵入方案。

与 EA 图形面板相比,指标子窗口按钮的优势在于:生命周期随指标依附的图表存在,不占用 Expert Advisors 的订单权限;适合纯展示、参数切换、信号静音等场景。缺点是子窗口高度受用户拖拽限制,且指标无法像 EA 那样直接下单,只能触发内部状态或外部全局变量。

✦ 对象类型选择
MQL5 可创建 OBJ_BUTTON、OBJ_BITMAP、OBJ_BITMAP_LABEL、OBJ_EDIT 等做按钮。本文选用 OBJ_EDIT 并设为只读,因为它能显示指定文本、边角锐利且易于着色,比原生 OBJ_BUTTON 更灵活。

二、基础框架:空子窗口指标骨架

使用 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();
 }
}
⚠ 常见坑
1. 忘记 ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true) 导致收不到悬停;2. 对象名无前缀被其他指标覆盖;3. 子窗口相对Y未减偏移量,悬停判定错位;4. 多指标同图表互相关闭事件跟踪。

十、扩展方向与对比总结

此面板可扩展为策略开关、周期切换、信息弹窗触发。相比用 OBJ_BUTTON,OBJ_EDIT 只读方案在文字与配色上更统一;相比 EA 面板,它轻量但无交易权限。开发者可结合后续文章的滚动条、多图表控件构建完整终端内 GUI。原文附 TestButtons.mq5 可下载实践。

常见问题

因为图表可能同时运行多个程序,若对象名相同会互相覆盖或误删。原文用指标短名+下划线作为 prefix,在 AddPrefix 中给所有 button_object_names 加上,确保唯一性。
在 CHARTEVENT_MOUSE_MOVE 中用 ChartXYToTimePrice 得到 window 和绝对 y,再用 chart_y_offset=ChartGetInteger(0,CHART_WINDOW_YDISTANCE,subwindow_number) 获取顶部距离,y-=chart_y_offset 即为子窗口内相对坐标。
因为 OnDeinit 里调用了 ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,false) 是全局设置。多程序同图表时,原文建议在 OnTimer 里用 CheckChartEventMouseMove 每秒检测并重新启用。
常见原因:未启用 CHART_EVENT_MOUSE_MOVE;Y坐标未减子窗口偏移导致判定区域错;按钮状态数组已为 true 被 continue 跳过;或忘记 ChartRedraw 重绘。