基于 CChartObject 设计新 GUI 组件:从基类继承到自定义控件的落地路径(基础篇)
(1/3)· 不用裸写 ObjectCreate 也能造出进度条、微调框和表格,这篇先把标准库地基铺平
很多交易者想给指标或 EA 加个进度条、数字微调框,结果一头扎进 ObjectCreate 和一堆 ObjectSetInteger 里,属性名记混、对象生命周期漏管,界面没跑起来先崩了图表。其实 MQL5 标准库的 CChartObject 体系已经把图表对象封装成了类,继承一下就能少写几百行胶水代码。
从 CChartObject 派生自己的 GUI 控件
在 MT5 里做自定义图表控件,最稳的底座是标准库里的 CChartObject 类。它封装了图表对象共有的坐标、可见性、鼠标事件钩子,你只要继承它就能避开从头处理 OBJPROP 那堆杂属性。 实际项目里,直接 new 一个 CChartObject 的子类对象挂到当前图表,往往比用 OBJ_LABEL 手动拼字符串更可控。官方标准库自 2013 年起就随终端分发,当前 MT5 build 以上均内置,无需额外下载。 下面这段是从 CChartObject 继承出最小可用控件的骨架,编译后拖进任意图表就能看到一个绑定到坐标的空对象。外汇与贵金属图表对象仅作界面承载,不涉及下单逻辑,但实操中误触对象事件可能干扰手动下单,仍属高风险环境,建议先在模拟盘验证。
class CMyBox : class="kw">public CChartObject { class="kw">public: CMyBox(class="type">void) {} ~CMyBox(class="type">void) {} class="type">bool Create(class="kw">const class="type">long chart, 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) { if(!CChartObject::Create(chart,name,window,x,y)) class="kw">return class="kw">false; ObjectSetInteger(chart,name,OBJPROP_BGCOLOR,0xFF0000); class="kw">return true; } };
「从半自动 EA 到标准库 GUI 重构」
前作用 GUI 做半自动 EA 交易后,实盘复杂度一上来就暴露了老界面的瓶颈:指标和 EA 逻辑越叠越多,手动拼的控件根本撑不住。 回头啃完 MQL5 标准库的类结构,我把交互层重做了一遍,新塞了三个派生类进去:CChartObjectProgressBar、CChartObjectSpinner 和 CChartEditTable。 其中 CChartEditTable 直接用了动态二维对象数组来管表格单元格,这算是 MQL5 里跑通动态二维对象数组的一个现成参考实现,想写自定义表控件的可以照这个类拆。
◍ 用类封装代替裸对象函数
在 MT5 里如果不碰标准库,画任何图表对象都得靠 ObjectCreate() 打底,类型用 ENUM_OBJECT 传进去,之后属性靠 ObjectGetInteger / ObjectSetInteger、ObjectGetDouble / ObjectSetDouble、ObjectGetString / ObjectSetString 这六件套读写。对象还能删、移、计数,但函数名散、参数顺序死,写多就乱。 CChartObject 是所有图形对象的基类,后代类靠加变量和方法扩出趋势线、通道、回归线等。以 CChartObjectTrend 为例,它管 OBJ_TREND 类型,向下派生了 CChartObjectTrendByAngle、CChartObjectChannel、CChartObjectStdDevChannel、CChartObjectRegression、CChartObjectPitchfork 五个子类,各自只改 Create()、Type() 或加个别属性修饰符。 裸写标签要调 ObjectSetInteger / ObjectGetInteger 传字符串名和 OBJPROP 常量;换成 CChartObjectLabel 声明实例后直接点方法,编辑器还会补全,不用翻文档查属性宏。下面这段趋势线基类代码,把 RayLeft 和 Create 的实现拆开看就明白封装省了什么。 代码里 RayLeft(bool) 先判 m_chart_id==-1 防未绑定,再调 ObjectSetInteger 写 OBJPROP_RAY_LEFT;Create 用 ObjectCreate 建 OBJ_TREND,成功才 Attach 绑定到图表窗口。继承写新对象类时,最低成本就是只覆盖 Create() 和 Type(),属性访问符按需加。 外汇和贵金属图表对象受报价跳空影响,射线类对象在重大数据行情中可能偏离预期锚点,挂对象前先确认窗口与图表 ID 一致。
class CChartObjectTrend : class="kw">public CChartObject { class="kw">public: class=class="str">"cmt">//--- 访问对象属性的方法 class="type">bool RayLeft() class="kw">const; class="type">bool RayLeft(class="type">bool new_sel); class="type">bool RayRight() class="kw">const; class="type">bool RayRight(class="type">bool new_sel); class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window, class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="type">class="kw">datetime time2,class="type">class="kw">double price2); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() class="kw">const { class="kw">return(OBJ_TREND); } class=class="str">"cmt">//--- 操作文件方法 class="kw">virtual class="type">bool Save(class="type">int file_handle); class="kw">virtual class="type">bool Load(class="type">int file_handle); }; class="type">bool CChartObjectTrend::RayLeft(class="type">bool new_ray) { class=class="str">"cmt">//--- 检查 if(m_chart_id==-class="num">1) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_LEFT,new_ray)); } class="type">bool CChartObjectTrend::Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window, class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="type">class="kw">datetime time2,class="type">class="kw">double price2) { class="type">bool result=ObjectCreate(chart_id,name,OBJ_TREND,window,time1,price1,time2,price2); if(result) result&=Attach(chart_id,name,window,class="num">2); class=class="str">"cmt">//--- class="kw">return(result); } class="type">bool CChartObjectTrend::Save(class="type">int file_handle) { class="type">bool result; class=class="str">"cmt">//--- 检查 if(file_handle<=class="num">0) class="kw">return(class="kw">false);
趋势线的序列化与角度/通道子类
把趋势线对象存进文件时,除了基类 CChartObject 的通用字段,还要额外落盘两条射线开关:OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT,各占 1 字节(CHAR_VALUE 长度 = sizeof(char))。Save 里先判 m_chart_id==-1 直接返 false,再写基类,随后用 FileWriteInteger 按 char 写入左右射线属性,任何一次写入字节数不符就中断返回 false。 Load 是逆过程:文件句柄小于等于 0 或图表 ID 无效直接退出;基类读成功后,用 FileReadInteger(file_handle,CHAR_VALUE) 取回这两个开关,并通过 ObjectSetInteger 还原到当前对象。若设置失败同样立即 return(false),保证磁盘与内存里的射线状态一致。 在 CChartObjectTrend 之上,标准库还派生了三个常用子类:CChartObjectTrendByAngle 增加 Angle() 访问器与按角度创建的 Create,类型枚举为 OBJ_TRENDBYANGLE;CChartObjectChannel 用三点 Create 构造等距通道(OBJ_CHANNEL);CChartObjectStdDevChannel 则暴露 Deviations() 来读写标准差倍数。外汇与贵金属图表上画这些对象属高风险操作,参数误设可能导致信号失效。
if(m_chart_id==-class="num">1) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 写下 result=CChartObject::Save(file_handle); if(result) { class=class="str">"cmt">//--- 写下 "Ray left" 属性值 if(FileWriteInteger(file_handle,(class="type">int) ObjectGetInteger(m_chart_id,m_name, OBJPROP_RAY_LEFT),CHAR_VALUE)!=class="kw">sizeof(class="type">char)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 写下 "Ray right" 属性值 if(FileWriteInteger(file_handle,(class="type">int) ObjectGetInteger(m_chart_id,m_name, OBJPROP_RAY_RIGHT),CHAR_VALUE)!=class="kw">sizeof(class="type">char)) class="kw">return(class="kw">false); } class=class="str">"cmt">//--- class="kw">return(result); } class="type">bool CChartObjectTrend::Load(class="type">int file_handle) { class="type">bool result; class=class="str">"cmt">//--- 检查 if(file_handle<=class="num">0) class="kw">return(class="kw">false); if(m_chart_id==-class="num">1) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 读取 result=CChartObject::Load(file_handle); if(result) { class=class="str">"cmt">//--- 读取 "Ray left" 属性值 if(!ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_LEFT,FileReadInteger(file_handle,CHAR_VALUE)))class="kw">return(class="kw">false); class=class="str">"cmt">//--- 读取 "Ray right" 属性值 if(!ObjectSetInteger(m_chart_id,m_name,OBJPROP_RAY_RIGHT,FileReadInteger(file_handle,CHAR_VALUE))) class="kw">return(class="kw">false); } class=class="str">"cmt">//--- class="kw">return(result); } class CChartObjectTrendByAngle : class="kw">public CChartObjectTrend { class="kw">public: class=class="str">"cmt">//--- 访问对象属性的方法 class="type">class="kw">double Angle() class="kw">const; class="type">bool Angle(class="type">class="kw">double angle); class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="type">class="kw">datetime time2,class="type">class="kw">double price2); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() { class="kw">return(OBJ_TRENDBYANGLE); } }; class CChartObjectChannel : class="kw">public CChartObjectTrend { class="kw">public: class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">class="kw">datetime time1,class="type">class="kw">double price1, class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="type">class="kw">datetime time3,class="type">class="kw">double price3); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() class="kw">const { class="kw">return(OBJ_CHANNEL); } }; class CChartObjectStdDevChannel : class="kw">public CChartObjectTrend { class="kw">public: class=class="str">"cmt">//--- 访问对象属性的方法 class="type">class="kw">double Deviations() class="kw">const; class="type">bool Deviations(class="type">class="kw">double deviation);
「用类封装把通道和叉形画线固化下来」
标准偏差通道类从趋势线基类派生,Create 需要传入两个时间点和偏差值 deviation,Type 方法直接返回 OBJ_STDDEVCHANNEL,等于在对象系统里给它打了一个可识别的标签。回归线类 CChartObjectRegression 更轻,只要 time1、time2 就能建对象,Type 返回 OBJ_REGRESSION,适合做均势带的可视化。 Pitchfork 叉形类最吃参数:time1/price1、time2/price2、time3/price3 三组坐标全要手动给,但 Type 返回的是 OBJ_CHANNEL 而非独立类型,说明 MT5 底层把它归在通道家族。外汇与贵金属波动大,这类手动锚点画线在高杠杆下误判风险很高,参数错了图形就偏。 OnStart 里那段是验证思路的捷径:先用 ObjectFind 查 my_OBJ_LABEL_object 是否存在,找不到就打印错误码并现场 ObjectCreate 一个 OBJ_LABEL,再把 XDISTANCE 设 200、YDISTANCE 设 300、颜色 White、文本 UP。你开 MT5 把这段贴进脚本跑一次,就能确认标签对象从零创建的完整调用链。
class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window, class="type">class="kw">datetime time1,class="type">class="kw">datetime time2,class="type">class="kw">double deviation); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() class="kw">const { class="kw">return(OBJ_STDDEVCHANNEL); } class=class="str">"cmt">//--- 操作文件方法 class="kw">virtual class="type">bool Save(class="type">int file_handle); class="kw">virtual class="type">bool Load(class="type">int file_handle); }; class CChartObjectRegression : class="kw">public CChartObjectTrend { class="kw">public: class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">class="kw">datetime time1,class="type">class="kw">datetime time2); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() class="kw">const { class="kw">return(OBJ_REGRESSION); } }; class CChartObjectPitchfork : class="kw">public CChartObjectTrend { class="kw">public: class=class="str">"cmt">//--- 创建对象方法 class="type">bool Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">class="kw">datetime time1,class="type">class="kw">double price1,class="type">class="kw">datetime time2,class="type">class="kw">double price2,class="type">class="kw">datetime time3,class="type">class="kw">double price3); class=class="str">"cmt">//--- 识别对象方法 class="kw">virtual class="type">int Type() class="kw">const { class="kw">return(OBJ_CHANNEL); } }; class="type">void OnStart() { class=class="str">"cmt">//--- class="type">class="kw">string label_name="my_OBJ_LABEL_object"; if(ObjectFind(class="num">0,label_name)<class="num">0) { Print("对象 ",label_name," 没有找到. 错误代码 = ",GetLastError()); ObjectCreate(class="num">0,label_name,OBJ_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,label_name,OBJPROP_XDISTANCE,class="num">200); ObjectSetInteger(class="num">0,label_name,OBJPROP_YDISTANCE,class="num">300); ObjectSetInteger(class="num">0,label_name,OBJPROP_COLOR,White); ObjectSetString(class="num">0,label_name,OBJPROP_TEXT,UP);