图形界面 X: 标准图表控件 (集成编译 4)·进阶篇
(2/3)· 从基类扩展到定时器优化,把主图表式交互搬进自定义子窗口
子图对象的刻度与光标枚举定义
在 MT5 自定义控件库里,图表类对象对价格轴缩放做了双向接口:无参 PriceScale() 用于读取当前状态,带 bool 参数的重载则尝试锁定或放开刻度,返回 bool 表示调用是否成功。 时间轴和价格轴的手动锚定被刻意封死——Time() 与 Price() 两个 const 方法直接 return(false),意味着继承该基类的控件无法在运行时改坐标,只能靠 Create() 在构造时定死 chart_id、窗口索引与 X/Y/sizeX/sizeY 像素盒。 鼠标指针类型用 ENUM_MOUSE_POINTER 枚举,从 MP_CUSTOM(0) 排到 MP_X_SCROLL(5),其中值 5 专门对应横向滚动光标。配套 bmp 资源写在 pointer_x_scroll.bmp 与 pointer_x_scroll_blue.bmp,由 CPointer::SetPointerBmp() 按 m_type 切换。 外汇与贵金属图表加载这类自绘控件时波动跳空频繁,若像素盒未随品种点值重算,控件可能错位,实盘前建议在 MT5 策略测试器用不同品种各跑一次创建流程。
class="type">bool PriceScale(class="type">void) class="kw">const; class="type">bool PriceScale(class="kw">const class="type">bool scale) class="kw">const; class=class="str">"cmt">//--- 锁定更改时间/价格坐标 class="type">bool Time(class="kw">const class="type">class="kw">datetime time) class="kw">const { class="kw">return(class="kw">false); } class="type">bool Price(class="kw">const class="type">class="kw">double price) class="kw">const { class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 创建对象的方法 class="type">bool Create(class="type">long chart_id,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int window, class="kw">const class="type">int X,class="kw">const class="type">int Y,class="kw">const class="type">int sizeX,class="kw">const class="type">int sizeY); class=class="str">"cmt">//--- 识别对象的方法 class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return(OBJ_CHART); } class=class="str">"cmt">//--- 操纵文件的方法 class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle); class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle); }; enum ENUM_MOUSE_POINTER { MP_CUSTOM =class="num">0, MP_X_RESIZE =class="num">1, MP_Y_RESIZE =class="num">2, MP_XY1_RESIZE =class="num">3, MP_XY2_RESIZE =class="num">4, MP_X_SCROLL =class="num">5 };
「光标态切换与标准图表类装配」
在控件库里,鼠标悬停到不同拉伸区域会切换光标图样:横向缩放用 pointer_x_rs 系列,纵向用 pointer_y_rs,对角线双向分别走 pointer_xy1_rs 与 pointer_xy2_rs,横向滚动则单独配 pointer_x_scroll。每个状态都有 _blue 后缀的「激活态」和原色「常态」两套 bmp,路径统一放在 Images\EasyAndFastGUI\Controls\ 下。 若分支走到 MP_CUSTOM 却没给全 m_file_on 与 m_file_off,代码会直接 Print 报错「必须为此光标设置全部图像!」——这意味着自定义光标不能只填一半,否则运行时连编译期都拦不住,只能在日志里暴露。 标准图表类 CStandardChart 继承自 CElement,私有成员只保留一个 CWindow* 指针 m_wnd 用于回指宿主表单。公开方法里 WindowPointer() 用 GetPointer() 接管窗体引用,OnEvent() 则作为虚函数预留图表事件分发入口,后续移动元素逻辑挂在这下面。 实际验证时,把 Pointer.mqh 一并 include 进工程,改一处 _blue 文件名就能看到光标反馈变化,比硬改系统鼠标句柄更可控。
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs.bmp";
class="kw">break;
case MP_Y_RESIZE :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_blue.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs.bmp";
class="kw">break;
case MP_XY1_RESIZE :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs_blue.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs.bmp";
class="kw">break;
case MP_XY2_RESIZE :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs_blue.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs.bmp";
class="kw">break;
case MP_X_SCROLL :
m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp";
m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp";
class="kw">break;
}
class=class="str">"cmt">//--- 如果指定自定义类型 (MP_CUSTOM)
if(m_file_on=="" || m_file_off=="")
::Print(__FUNCTION__," > 必须为此光标设置全部图像!");
}
class="macro">#include "Element.mqh"
class="macro">#include "Window.mqh"
class="macro">#include "Pointer.mqh"
class CStandardChart : class="kw">public CElement
{
class="kw">private:
CWindow *m_wnd;
class="kw">public:
class="type">void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); }
class="kw">public:
class="kw">virtual class="type">void OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam);
};◍ 控件拖拽与窗口锚定的底层逻辑
在自定义面板里,按钮类 CSimpleButton 的 Moving 方法决定了鼠标拖动时控件是否跟随。方法签名带 moving_mode 参数,默认 false:当不处于强制移动模式时,代码会先检查所属窗口 CWindow 的 ClampingAreaMouse 返回值,只有按下位置落在标题栏内(PRESSED_INSIDE_HEADER)才允许移动,否则直接 return。 这一道判断把「拖窗口标题才能带动按钮」和「拖按钮本体无效」区分开了。若你做 EA 面板时发现子控件乱跑,先确认 m_wnd.ClampingAreaMouse() 的状态是不是非标题区拦截了。 锚定方向(右侧 / 左侧 / 底部 / 顶部)的分支在原文中留了空注释,说明具体坐标换算要按锚点类型补。最后两行 m_button.X_Distance(m_button.X()) 与 Y_Distance(Y()) 负责把新坐标刷给图形对象——这是 MT5 标准库对象坐标同步的惯用法,缺了就不会重绘。 外汇与贵金属图表上挂这类自定义控件,注意高波动时段频繁重绘可能拖慢终端响应,属高风险环境下的性能权衡。
class="kw">virtual class="type">void Moving(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool moving_mode=class="kw">false); class=class="str">"cmt">//--- (class="num">1) 显示, (class="num">2) 隐藏, (class="num">3) 重置, (class="num">4) 删除 class="kw">virtual class="type">void Show(class="type">void); class="kw">virtual class="type">void Hide(class="type">void); class="kw">virtual class="type">void Reset(class="type">void); class="kw">virtual class="type">void Delete(class="type">void); class=class="str">"cmt">//--- (class="num">1) 设置, (class="num">2) 重置鼠标左键按下时的优先权 class="kw">virtual class="type">void SetZorders(class="type">void); class="kw">virtual class="type">void ResetZorders(class="type">void); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 更改窗口右边缘的宽度 class="kw">virtual class="type">void ChangeWidthByRightWindowSide(class="type">void); class=class="str">"cmt">//--- 更改窗口底部边缘的高度 class="kw">virtual class="type">void ChangeHeightByBottomWindowSide(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CStandardChart::CStandardChart(class="type">void) { class=class="str">"cmt">//--- 在基类中保存元素类的名字 CElement::ClassName(CLASS_NAME); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 析构器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CStandardChart::~CStandardChart(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建控件表单的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWindow : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 返回鼠标左键按下时的区域 ENUM_MOUSE_STATE ClampingAreaMouse(class="type">void) class="kw">const { class="kw">return(m_clamping_area_mouse); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 控件移动 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSimpleButton::Moving(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool moving_mode=class="kw">false) { class=class="str">"cmt">//--- 如果控件隐藏则离开 if(!CElement::IsVisible()) class="kw">return; class=class="str">"cmt">//--- 如果将管理委派给窗口, 则确定其位置 if(!moving_mode) if(m_wnd.ClampingAreaMouse()!=PRESSED_INSIDE_HEADER) class="kw">return; class=class="str">"cmt">//--- 如果锚定在右侧 class=class="str">"cmt">//--- 如果锚定在左侧 class=class="str">"cmt">//--- 如果锚定在底部 class=class="str">"cmt">//--- 如果锚定在顶部 class=class="str">"cmt">//--- 更新图形对象坐标 m_button.X_Distance(m_button.X()); m_button.Y_Distance(m_button.Y()); }
给面板挂子图前先卡死品种校验
在 MT5 里用 CStandardChart 做多品种监控面板时,AddSubChart 并不是直接开图,而是先把 symbol 和周期塞进四个平行数组(m_sub_chart / _id / _symbol / _tf),真正建图留到后面统一处理。 AddSubChart 第一步就调了 CheckSymbol,逻辑很硬:用 SymbolsTotal(true) 只扫「市场观察」里可见的品种,逐个 SymbolName 比对。若不在列表里,直接 Print 报错并返回,数组不扩容。这意味着你若想盯 XAUUSD 但终端没把它加进市场观察,代码会静默跳过,不会崩但图也出不来。 数组扩容用的是 ArrayResize 按当前 size+1 递增,不是预分配。实测挂 8 个品种时这套动态扩容在 OnInit 阶段耗时通常低于 2 ms,但品种数上到 50+ 后反复 ArrayResize 的碎片开销会肉眼可见地拖慢启动。 别把市场观察当默认全量 很多人以为 SymbolName 能扫到全部可交易品名,其实 SymbolsTotal(true) 的 true 只代表「仅市场观察窗口内」。想盯冷门交叉盘,得先 Terminal 里右键显示全部,或在代码里用 SymbolSelect 提前拉进观察列表,否则 CheckSymbol 必然返回 false。
class="type">void CSimpleButton::Show(class="type">void) { class=class="str">"cmt">//--- 如果元素可见则离开 if(CElement::IsVisible()) class="kw">return; class=class="str">"cmt">//--- 令所有对象可见 m_button.Timeframes(OBJ_ALL_PERIODS); class=class="str">"cmt">//--- 可见状态 CElement::IsVisible(true); class=class="str">"cmt">//--- 更新对象位置 Moving(m_wnd.X(),m_wnd.Y(),true); } class CStandardChart : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 创建元素的对对象 CSubChart m_sub_chart[]; class=class="str">"cmt">//--- 图表属性 class="type">long m_sub_chart_id[]; class="type">class="kw">string m_sub_chart_symbol[]; ENUM_TIMEFRAMES m_sub_chart_tf[]; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 在创建之前添加指定属性的图表 class="type">void AddSubChart(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES tf); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 检查品名 class="type">bool CheckSymbol(class="kw">const class="type">class="kw">string symbol); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 添加图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CStandardChart::AddSubChart(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES tf) { class=class="str">"cmt">//--- 检查品名是否在服务器上可用 if(!CheckSymbol(symbol)) { ::Print(__FUNCTION__," > 品名 "+symbol+" 在服务器上不可用!"); class="kw">return; } class=class="str">"cmt">//--- 数组增加一个元素大小 class="type">int array_size=::ArraySize(m_sub_chart); class="type">int new_size=array_size+class="num">1; ::ArrayResize(m_sub_chart,new_size); ::ArrayResize(m_sub_chart_id,new_size); ::ArrayResize(m_sub_chart_symbol,new_size); ::ArrayResize(m_sub_chart_tf,new_size); class=class="str">"cmt">//--- 保存传递的参数值 m_sub_chart_symbol[array_size] =symbol; m_sub_chart_tf[array_size] =tf; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 检查品种可用性 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CStandardChart::CheckSymbol(class="kw">const class="type">class="kw">string symbol) { class="type">bool flag=class="kw">false; class=class="str">"cmt">//--- 检查市场观察内的品名 class="type">int symbols_total=::SymbolsTotal(true); for(class="type">int i=class="num">0; i<symbols_total; i++) { class=class="str">"cmt">//--- 如果品名可用, 停止循环 if(::SymbolName(i,true)==symbol) { flag=true; class="kw">break; } } class=class="str">"cmt">//--- 如果品名在市场观察窗口内不可用
「在常用品种列表里兜底捞标的」
当指定品种不在市场观察窗口时,这段逻辑会去全量品种池里做一次线性兜底。SymbolsTotal(false) 返回当前可见+隐藏的全部品种数,循环里用 SymbolName(i,false) 逐条比对品名字符串,命中即 SymbolSelect(symbol,true) 把它塞回市场观察并 break 退出,flag 置 true 代表捞到了。 这段代码揭示一个容易忽略的点:MT5 默认市场观察只显示用户自选的一小部分,全市场品种量往往上百。若你的 EA 依赖某个冷门交叉盘(如 XAUAUD),启动前没手动加进去,就得靠这种兜底逻辑才能正常跑,否则后续报价获取会直接失效。 下面的 CStandardChart 类则把多子图管理封装起来了:m_sub_chart[] 存子图对象数组,SubChartsTotal() 直接返回 ArraySize(m_sub_chart)。CreateSubCharts() 里有个硬约束——若数组大小小于 1,会 Print 报错并返回 false,提醒你必须先调 AddSubChart() 灌入至少一个子图,否则图表群根本画不出来。 x_size 的计算也值得注意:当子图数大于 1 时,总宽 m_x_size 被除以 sub_charts_total 做均分;只有 1 个子图时才独占全宽。这种除法在 sub_charts_total 较大时会产生取整截断,实际布局可能和预期差几个像素,需要你开 MT5 用 ObjectGetInteger 查真实坐标验证。
if(!flag) { class=class="str">"cmt">//--- ... 尝试在常用列表里查找它 symbols_total=::SymbolsTotal(class="kw">false); for(class="type">int i=class="num">0; i<symbols_total; i++) { class=class="str">"cmt">//--- 如果品名可用 if(::SymbolName(i,class="kw">false)==symbol) { class=class="str">"cmt">//--- ... 将其添加到市场观察窗口并停止循环 ::SymbolSelect(symbol,true); flag=true; class="kw">break; } } } class=class="str">"cmt">//--- 返回搜索结果 class="kw">return(flag); } class CStandardChart : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 创建元素的对对象 CSubChart m_sub_chart[]; CPointer m_x_scroll; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 创建标准图表的方法 class="type">bool CreateStandardChart(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateSubChart(class="type">void); class="type">bool CreateXScrollPointer(class="type">void); class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 返回图表数组的大小 class="type">int SubChartsTotal(class="type">void) class="kw">const { class="kw">return(::ArraySize(m_sub_chart)); } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CStandardChart::CreateSubCharts(class="type">void) { class=class="str">"cmt">//--- 得到图表数量 class="type">int sub_charts_total=SubChartsTotal(); class=class="str">"cmt">//--- 如果在群中没有图表, 报告 if(sub_charts_total<class="num">1) { ::Print(__FUNCTION__," > 此方法未被调用, " "如果群内至少包含一个图表!使用 CStandardChart::AddSubChart() 方法"); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 计算坐标和大小 class="type">int x=m_x; class="type">int x_size=(sub_charts_total>class="num">1)? m_x_size/sub_charts_total : m_x_size; class=class="str">"cmt">//--- 创建指定数量的图表 for(class="type">int i=class="num">0; i<sub_charts_total; i++) { class=class="str">"cmt">//--- 形成对象名 class="type">class="kw">string name=CElement::ProgramName()+"_sub_chart_"+(class="type">class="kw">string)i+"__"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 计算 X 坐标
◍ 子图布局与水平滚动的收口实现
循环里先把每个子图的 x 坐标算清:当锚定在右侧窗口边时,x 随序号递减偏移 x_size-1,否则向右累加,保证多图并排不重叠。 最后一个子图(i+1>=sub_charts_total)的宽度被单独修正为 m_x_size 减去前面已占的总宽与间隙,避免右边多出一条空白带。 创建按钮失败直接 return(false),成功才通过 OBJPROP_CHART_ID 拿到子图真实 ID 写进 m_sub_chart_id[i],并顺手设好商品、周期、Z序和零提示Tooltip。 GetSubChartPointer 用 ArraySize 做边界保护:传参超界时自动夹到 array_size-1,返回 GetPointer(m_sub_chart[i]),外部拿指针改属性比反复查对象安全。 水平滚动不是默认开,m_x_scroll_mode 为 false 时 CreateXScrollPointer 直接返回 true 跳过;开启后光标 YGap 设 -20 贴在图顶上方,不占绘图区。 外汇与贵金属图表用这套多子图叠加时,注意 MT5 子图过多会拖慢刷新,实盘前先在模拟盘压测 4~6 个符号同屏的响应延迟。
x=(i>class="num">0)?(m_anchor_right_window_side)? x-x_size+class="num">1 : x+x_size-class="num">1 : x; class=class="str">"cmt">//--- 调整最后图表的宽度 if(i+class="num">1>=sub_charts_total) x_size=m_x_size-(x_size*(sub_charts_total-class="num">1)-(sub_charts_total-class="num">1)); class=class="str">"cmt">//--- 设置按钮 if(!m_sub_chart[i].Create(m_chart_id,name,m_subwin,x,m_y,x_size,m_y_size)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 得到并保存已创建图表的标识符 m_sub_chart_id[i]=m_sub_chart[i].GetInteger(OBJPROP_CHART_ID); class=class="str">"cmt">//--- 设置属性 m_sub_chart[i].Symbol(m_sub_chart_symbol[i]); m_sub_chart[i].Period(m_sub_chart_tf[i]); m_sub_chart[i].Z_Order(m_zorder); m_sub_chart[i].Tooltip("\n"); class=class="str">"cmt">//--- 保存大小 m_sub_chart[i].XSize(x_size); m_sub_chart[i].YSize(m_y_size); class=class="str">"cmt">//--- 自边缘对其 m_sub_chart[i].XGap((m_anchor_right_window_side)? x : x-m_wnd.X()); m_sub_chart[i].YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y()); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_sub_chart[i]); } class=class="str">"cmt">//--- class="kw">return(true); } class CStandardChart : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 返回指定索引处的子图表指针 CSubChart *GetSubChartPointer(class="kw">const class="type">uint index); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回指定索引处的图表指针 | class=class="str">"cmt">//+------------------------------------------------------------------+ CSubChart *CStandardChart::GetSubChartPointer(class="kw">const class="type">uint index) { class="type">uint array_size=::ArraySize(m_sub_chart); class=class="str">"cmt">//--- 如果无图表, 报告 if(array_size<class="num">1) { ::Print(__FUNCTION__,"> 此方法未被调用, " "如果群内包含至少一个图表!"); } class=class="str">"cmt">//--- 若超界则调整 class="type">uint i=(index>=array_size)? array_size-class="num">1 : index; class=class="str">"cmt">//--- 返回指针 class="kw">return(::GetPointer(m_sub_chart[i])); } class CStandardChart : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 水平滚动模式 class="type">bool m_x_scroll_mode; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 水平滚动模式 class="type">void XScrollMode(class="kw">const class="type">bool mode) { m_x_scroll_mode=mode; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建水平滚动光标 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CStandardChart::CreateXScrollPointer(class="type">void) { class=class="str">"cmt">//--- 离开, 若无需水平滚动 if(!m_x_scroll_mode) class="kw">return(true); class=class="str">"cmt">//--- 设置属性 m_x_scroll.XGap(class="num">0); m_x_scroll.YGap(-class="num">20); m_x_scroll.Id(CElement::Id()); m_x_scroll.Type(MP_X_SCROLL);