基于 CChartObject 设计新 GUI 组件:从基类继承到自定义控件的落地路径(基础篇)
🧩

基于 CChartObject 设计新 GUI 组件:从基类继承到自定义控件的落地路径(基础篇)

(1/3)· 不用裸写 ObjectCreate 也能造出进度条、微调框和表格,这篇先把标准库地基铺平

实战向 第 1/3 篇

很多交易者想给指标或 EA 加个进度条、数字微调框,结果一头扎进 ObjectCreate 和一堆 ObjectSetInteger 里,属性名记混、对象生命周期漏管,界面没跑起来先崩了图表。其实 MQL5 标准库的 CChartObject 体系已经把图表对象封装成了类,继承一下就能少写几百行胶水代码。

从 CChartObject 派生自己的 GUI 控件

在 MT5 里做自定义图表控件,最稳的底座是标准库里的 CChartObject 类。它封装了图表对象共有的坐标、可见性、鼠标事件钩子,你只要继承它就能避开从头处理 OBJPROP 那堆杂属性。 实际项目里,直接 new 一个 CChartObject 的子类对象挂到当前图表,往往比用 OBJ_LABEL 手动拼字符串更可控。官方标准库自 2013 年起就随终端分发,当前 MT5 build 以上均内置,无需额外下载。 下面这段是从 CChartObject 继承出最小可用控件的骨架,编译后拖进任意图表就能看到一个绑定到坐标的空对象。外汇与贵金属图表对象仅作界面承载,不涉及下单逻辑,但实操中误触对象事件可能干扰手动下单,仍属高风险环境,建议先在模拟盘验证。

MQL5 / C++
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 一致。

MQL5 / C++
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() 来读写标准差倍数。外汇与贵金属图表上画这些对象属高风险操作,参数误设可能导致信号失效。

MQL5 / C++
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 把这段贴进脚本跑一次,就能确认标签对象从零创建的完整调用链。

MQL5 / C++
  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);
把控件样板代码交给小布
这些标准库控件的初始化与刷新逻辑,小布盯盘的 AIGC 已内置常用模板,打开对应品种页的脚本区就能直接调出骨架,你只需填业务参数。

常见问题

CChartObject 用类封装了创建、属性读写和销毁,后代类扩展新方法;裸用 ObjectCreate 要手动调 ObjectSetInteger/Double/String 并自己维护生命周期。
带三角表示它是其他类的父类,仅作为基类扩展可能性,不直接实例化为具体图表对象。
可派生 CChartObjectTrendByAngle、CChartObjectChannel、CChartObjectStdDevChannel、CChartObjectRegression 和 CChartObjectPitchfork 等。
小布内置了标准库 GUI 控件的片段模板,描述需要的控件与交互后可由 AIGC 产出基础类代码,但仍需在 MT5 里按品种校验。
可用动态二维对象指针数组承载单元格对象,运行时按行列伸缩,CChartEditTable 就是基于该思路的实施示例,本篇仅铺垫概念,实战见后续篇。