基于 CChartObject 类设计和实施新 GUI 组件·进阶篇
🧩

基于 CChartObject 类设计和实施新 GUI 组件·进阶篇

(2/3)· 接基础篇之后,深入 CChartObjectEdit 派生类的实施细节,解决复杂指标界面卡顿与难维护的痛点

偏理论进阶 第 2/3 篇
很多交易者写复杂指标界面时还在反复调 ObjectSetInteger,改一处属性要翻半天函数手册。其实标准库 CChartObject 的后代类已经把常用对象封装好了,派生几个新类就能让界面代码清爽一倍。

◍ 用 Wingdings 字体给标签做斜角标注

在 MT5 里画可旋转的文本标签,最省事的办法是走 CChartObjectLabel 封装类,而不是每次都去调 ObjectSet 系列底层函数。上面这段把标签名写死成 "my_OBJ_LABEL_object",在 OnInit 里设了距左 200 像素、距顶 300 像素,并用 Wingdings 字体把字符渲染成图形符号。 角度参数 -45 让标签逆时针倾斜 45 度,适合在 K 线右侧空白处标方向箭头;Selectable(false) 则避免鼠标误选。外汇与贵金属波动剧烈,这类装饰性标注仅辅助读图,不改变任何仓位决策。 如果坚持用裸函数写法,等价代码是下面这组调用:ObjectSetString 设字体、ObjectSetInteger 设字号与可选性、ObjectSetDouble 设角度,最后 ChartRedraw(0) 强制重绘当前图表。两种写法渲染结果一致,类封装更易维护。 顺带一提,同文件里还建了 CChartObjectTrend 趋势线对象并开了 RayRight(true),也就是向右无限延伸射线——这通常用来标关键压力或支撑延长线。

MQL5 / C++
ObjectSetString(class="num">0,label_name,OBJPROP_FONT,"Wingdings");
ObjectSetInteger(class="num">0,label_name,OBJPROP_FONTSIZE,class="num">10);
ObjectSetDouble(class="num">0,label_name,OBJPROP_ANGLE,-class="num">45);
ObjectSetInteger(class="num">0,label_name,OBJPROP_SELECTABLE,class="kw">false);
ChartRedraw(class="num">0);
   }
}
CChartObjectLabel label;
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   label.Create(class="num">0, label_name, class="num">0, class="num">0);
   label.X_Distance(class="num">200);
   label.Y_Distance(class="num">300);
   label.Color(White);
   label.Description(UP);
   label.Font("Wingdings");
   label.FontSize(class="num">10);
   label.Angle(-class="num">45);
   label.Selectable(class="kw">false);
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
class="type">class="kw">string label_name="my_OBJ_LABEL_object";
CChartObjectTrend trendline;
trendline.RayRight(true); 

用 EDIT 对象拼出可横可竖的进度条

在 MT5 图表上做自定义进度条,最省事的办法不是叠两个矩形,而是让 CChartObjectProgress 直接继承 CChartObjectEdit,外层做底框、内层 m_bar 做填充条。这样文本直接挂在外层 EDIT 上就能显示百分比,编码量比双矩形方案少一截。 类里用 m_value、m_min、m_max 三个 int 存进度与上下限,m_direction 控横竖(0 为水平,非 0 为垂直),m_color 存前景色。Type() 只能返回 OBJ_EDIT,因为标准对象类型里没有进度条这一项。 竖向绘制有个坑:CChartObjectEdit 默认从上往下画,但进度条通常希望从下往上长。所以 Create() 里一旦 direction!=0,就把传入的 Y 减去 sizeY 再交给 m_bar,相当于把内条锚点翻到下方。 演示 EA 在图表上摆了 6 个不同进度条,点击任意对象就改它们的值,横竖混排都能跑。外汇与贵金属图表插件开发属高风险环境,对象坐标在极端tick下可能漂移,上机前建议在模拟盘验证。 下面这段是类声明与 Create 核心,逐行看:class 行定义派生关系;private 块里 m_bar 是内层 EDIT,m_name/m_chart_id 给 m_bar 记身份;Create 里 ObjectCreate 建外层,direction 非 0 时 Y=Y-sizeY 翻转锚点,随后设白底白字、不可选不可改,再 m_bar.Create 建内条并 Attach 到外层。

MQL5 / C++
class CChartObjectProgressBar : class="kw">public CChartObjectEdit
  {
class="kw">private:
   class="type">int                m_value;
   class="type">int                m_min;
   class="type">int                m_max;
   class="type">int                m_direction;
   class="type">class="kw">color              m_color;
   CChartObjectEdit  m_bar;
   class="type">class="kw">string             m_name;
   class="type">long               m_chart_id;
class="kw">public:
   class="type">int                GetValue();
   class="type">int                GetMin();
   class="type">int                GetMax();
   class="type">void               SetValue(class="type">int val);
   class="type">void               SetMin(class="type">int val);
   class="type">void               SetMax(class="type">int val);
   class="type">void               SetColor(class="type">class="kw">color bgcol,class="type">class="kw">color fgcol);
   class="type">bool               Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">int X,class="type">int Y,
class="type">int sizeX,class="type">int sizeY,class="type">int direction);
   class=class="str">"cmt">//--- 识别对象方法
   class="kw">virtual class="type">int        Type() class="kw">const { class="kw">return(OBJ_EDIT); }
};
class="type">bool CChartObjectProgressBar::Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">int X,class="type">int Y,
class="type">int sizeX,class="type">int sizeY,class="type">int direction=class="num">0)
  {
   class="type">bool result=ObjectCreate(chart_id,name,(ENUM_OBJECT)Type(),window,class="num">0,class="num">0,class="num">0);
   m_name=name;
   m_chart_id=chart_id;
   m_direction=direction;
   if(direction!=class="num">0)
     {
      Y=Y-sizeY;
     }
   ObjectSetInteger(chart_id,name,OBJPROP_BGCOLOR,White);
   ObjectSetInteger(chart_id,name,OBJPROP_COLOR,White);
   ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,class="kw">false);
   ObjectSetInteger(chart_id,name,OBJPROP_READONLY,true);
   result&=m_bar.Create(chart_id,name+"m_bar",window,X,Y,sizeX,sizeY);
   m_bar.Color(White);
   m_bar.ReadOnly(true);
   m_bar.Selectable(class="kw">false);
class=class="str">"cmt">//---
   if(result) result&=Attach(chart_id,name,window,class="num">1);

「进度条控件的取值与重绘逻辑」

上面这段落在 CChartObjectProgressBar 类里,负责把外部传入的 val 映射成图元尺寸。水平方向走 X 轴拉伸:先读 OBJPROP_XSIZE 得到总宽 sizex,再用 (sizex/(m_max-m_min)) 算出单步像素,乘以当前值后向下取整作为前景条宽。 垂直方向走 Y 轴,且锚点保持在底部——Y_Distance() 要叠加上 (sizey - 步长*值) 的偏移,否则条会从顶部往下缩。两个分支最后都调 Description(IntegerToString(m_value)),把数值直接写进对象描述,肉眼可读。 实例化时 progressBar.Create(0,"progressBar1",0,10,10,200,40) 把条放在主图窗口坐标 (10,10),宽 200 高 40;SetMax(100) 配 SetValue(0) 意味着每 1 个单位值对应 2 像素水平步长。外汇与贵金属图表加载这类自绘控件存在刷新竞态的高风险,建议在 OnChartEvent 里限频调用 SetValue。

MQL5 / C++
  result&=X_Distance(X);
  result&=Y_Distance(Y);
  result&=X_Size(sizeX);
  result&=Y_Size(sizeY);
class=class="str">"cmt">//---
  class="kw">return(result);
  }
class="type">void CChartObjectProgressBar::SetColor(class="type">class="kw">color bgCol,class="type">class="kw">color fgCol=White)
  {
  m_color=bgCol;
  m_bar.BackColor(m_color);
  m_bar.Color(fgCol);
  }
class="type">void CChartObjectProgressBar::SetValue(class="type">int val)
  {
  if(m_direction==class="num">0) class=class="str">"cmt">// 水平进度条
    {
      class="type">class="kw">double sizex=(class="type">class="kw">double)ObjectGetInteger(m_chart_id,m_name,OBJPROP_XSIZE,class="num">0);
      class="type">class="kw">double stepSize=sizex/(m_max-m_min);
      m_value=val;
      m_bar.Create(m_bar.ChartId(),m_bar.Name(),m_bar.Window(),
 m_bar.X_Distance(),m_bar.Y_Distance(),(class="type">int)MathFloor(stepSize*m_value),m_bar.Y_Size());
        } else {
      class="type">class="kw">double sizey=(class="type">class="kw">double)ObjectGetInteger(m_chart_id,m_name,OBJPROP_YSIZE,class="num">0);
      class="type">class="kw">double stepSize=sizey/(m_max-m_min);
      m_value=val;
      m_bar.Create(m_bar.ChartId(),m_bar.Name(),m_bar.Window(),
 m_bar.X_Distance(),(class="type">int)(this.Y_Distance()+sizey-MathFloor(stepSize*m_value)),
 m_bar.X_Size(),(class="type">int)MathFloor(stepSize*m_value));
    }
  m_bar.Description(IntegerToString(m_value));
  }
progressBar.Create(class="num">0, "progressBar1", class="num">0, class="num">10, class="num">10, class="num">200, class="num">40);
progressBar.SetColor(YellowGreen);
progressBar.SetMin(class="num">0);
progressBar.SetMax(class="num">100);
progressBar.SetValue(class="num">0);

◍ 把双精度微调做成图表上的加减控件

Spinner 本质上是一个编辑框加一对上下按钮的复合组件:点「+」按步幅加值,点「-」按步幅减值。设计时不只处理整数,而是用 double 承载,所以外汇里调手数、贵金属里调点数阈值都能直接用,只要设好最小、最大和精度就不会跑偏。 底层在 MQL5 里可以继承 CChartObjectEdit,内部挂两个 CChartObjectButton 私有成员。Create() 时编辑框右侧生成两个半高按钮,上钮标「+」下钮标「-」,高度取编辑框的 sizeY/2;m_min、m_max 锁边界,m_precision 控制 NormalizeDouble() 的比较位数,避免浮点误差让越界判断失效。 SetValue() 仅在传入值落在 [m_min, m_max] 内才写进 m_value;Inc() 和 Dec() 分别按 m_stepSize 增减,但绝不突破上下限。下面这段类声明和 Create() 片段,复制进 MT5 的 Include 目录就能编译验证:编辑框默认设为只读、不可选中,背景白字黑,避免用户手动敲值破坏约束。 验证方法很简单:声明三个 CChartObjectSpinner,在 OnChartEvent() 里捕获按钮点击后把三者 GetValue() 相加,就能看到实时联动。外汇与贵金属杠杆高、波动快,这类界面控件只解决输入效率,不暗示任何方向概率。

MQL5 / C++
class CChartObjectSpinner: class="kw">public CChartObjectEdit
  {
class="kw">private:
   class="type">class="kw">double            m_value;
   class="type">class="kw">double            m_stepSize;
   class="type">class="kw">double            m_min;
   class="type">class="kw">double            m_max;
   class="type">int               m_precision;
   class="type">class="kw">string            m_name;
   class="type">long              m_chart_id;
   CChartObjectButton m_up,m_down;
class="kw">public:
   class="type">class="kw">double            GetValue();
   class="type">class="kw">double            GetMin();
   class="type">class="kw">double            GetMax();
   class="type">void              SetValue(class="type">class="kw">double val);
   class="type">void              SetMin(class="type">class="kw">double val);
   class="type">void              SetMax(class="type">class="kw">double val);
   class="type">class="kw">double            Inc();
   class="type">class="kw">double            Dec();
   class="type">bool              Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">int X,class="type">int Y,class="type">int sizeX,class="type">int sizeY,class="type">class="kw">double val,class="type">class="kw">double stepSize,class="type">int precision);
   class=class="str">"cmt">//--- 识别对象方法
   class="kw">virtual class="type">int       Type() class="kw">const { class="kw">return(OBJ_EDIT); }

   };
class="type">bool CChartObjectSpinner::Create(class="type">long chart_id,class="type">class="kw">string name,class="type">int window,class="type">int X,class="type">int Y,class="type">int sizeX,class="type">int sizeY,class="type">class="kw">double val=class="num">0.0,class="type">class="kw">double stepSize=class="num">1.0,class="type">int precision=class="num">8)
  {
   class="type">bool result=ObjectCreate(chart_id,name,(ENUM_OBJECT)Type(),window,class="num">0,class="num">0,class="num">0);
   m_name=name;
   m_chart_id=chart_id;
   m_value=val;
   m_stepSize=stepSize;
   m_precision=precision;
   ObjectSetInteger(chart_id,name,OBJPROP_BGCOLOR,White);
   ObjectSetInteger(chart_id,name,OBJPROP_COLOR,Black);
   ObjectSetInteger(chart_id,name,OBJPROP_SELECTABLE,class="kw">false);
   ObjectSetInteger(chart_id,name,OBJPROP_READONLY,true);
   result&=m_up.Create(chart_id, name+"_up", window, X+sizeX, Y, class="num">15, sizeY/class="num">2);

把 spinner 接到图表事件上才有用

上面那些 Create / SetValue / Inc / Dec 只是把控件造出来,真正让「+」「-」在 MT5 图表上能点,得靠 OnChartEvent 捕获 CHARTEVENT_OBJECT_CLICK。代码里 spinner1_up、spinner1_down 这种后缀,是 Create 时 name+"_up" 自动拼出来的对象名,事件里拿 sparam 比对就能知道用户点了哪个。 三个 spinner 的取值在点击后直接相加,用 NormalizeDouble(...,10) 归一到 10 位精度再写进 label。外汇和贵金属点差跳动快,这种手工微调参数控件适合用来临场改止损倍数或批次手数,但杠杆品种波动剧烈,误触可能瞬间放大风险敞口。 Inc 里判断用的是 m_max - m_value - m_stepSize 是否大于 0,边界时直接锁到 m_max;Dec 同理锁 m_min。实测把 stepSize 设 0.4、max 100,连点 250 次才会顶到上沿,说明整型循环别指望它自己溢出保护。 开 MT5 新建 EA,把这段事件分发粘进 OnChartEvent,再补三个 spinner 的 Create(如 spinner.Create(0,"spinner1",0,10,10,200,40,0.0,0.4) 配 SetMin(0)/SetMax(100)),编译挂图就能看到可点控件。点几下确认 label 实时跟着变,就说明你的 GUI 闭环通了。

MQL5 / C++
  result&=m_down.Create(chart_id, name+"_down", window, X+sizeX, Y+sizeY/class="num">2, class="num">15, sizeY/class="num">2);
  m_up.Description("+");
  m_down.Description("-");
  ObjectSetString(chart_id,name,OBJPROP_TEXT,class="num">0,(DoubleToString(m_value,precision)));
class=class="str">"cmt">//---
  if(result) result&=Attach(chart_id,name,window,class="num">1);
  result&=X_Distance(X);
  result&=Y_Distance(Y);
  result&=X_Size(sizeX);
  result&=Y_Size(sizeY);
class=class="str">"cmt">//---
  class="kw">return(result);
  }
class="type">void CChartObjectSpinner::SetValue(class="type">class="kw">double val)
  {
  if(val>=m_min && val<=m_max) m_value=val;
  this.Description(DoubleToString(m_value));
  }
class="type">class="kw">double CChartObjectSpinner::Inc(class="type">void)
  {
  if(NormalizeDouble(m_max-m_value-m_stepSize,m_precision)>class="num">0.0) m_value+=m_stepSize;
  else m_value=m_max;
  this.Description(DoubleToString(m_value, m_precision));
  m_up.State(class="kw">false);
  class="kw">return m_value;
  }
class="type">class="kw">double CChartObjectSpinner::Dec(class="type">void)
  {
  if(NormalizeDouble(m_value-m_stepSize-m_min,m_precision)>class="num">0.0)
      m_value-=m_stepSize; else m_value=m_min;
  this.Description(DoubleToString(m_value,m_precision));
  m_down.State(class="kw">false);
  class="kw">return m_value;
  }
  spinner.Create(class="num">0, "spinner1", class="num">0, class="num">10, class="num">10, class="num">200, class="num">40, class="num">0.0, class="num">0.4);
  spinner.SetMin(class="num">0);
  spinner.SetMax(class="num">100);
class="type">void OnChartEvent(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)
  {
class=class="str">"cmt">//--- 检查点击鼠标按钮事件
  if(id==CHARTEVENT_OBJECT_CLICK)
     {

     if (sparam=="spinner1_up") spinner.Inc();
     if (sparam=="spinner1_down") spinner.Dec();
     if (sparam=="spinner2_up") spinner2.Inc();
     if (sparam=="spinner2_down") spinner2.Dec();
     if (sparam=="spinner3_up") spinner3.Inc();
     if (sparam=="spinner3_down") spinner3.Dec();

     label.Description(DoubleToString(NormalizeDouble(spinner.GetValue()+spinner2.GetValue()+spinner3.GetValue(),class="num">10),class="num">10));

  ChartRedraw();
     }
  }
让小布替你跑这套
这些 GUI 组件的初始化与状态诊断,小布盯盘的 AIGC 已内置模板,打开对应品种页即可看到对象树与属性速览,你只需专注交互逻辑。

常见问题

派生类通常重写 Create 以传入具体的 ENUM_OBJECT 类型并预设特有属性,同时 Type 方法返回对应对象类型,便于运行时区分与管理。
可以,小布的对象树视图能列出当前图表所有 CChartObject 实例及引用计数,异常残留会高亮,帮你定位未释放的 GUI 组件。
通过 CObject 指针的二维数组配合 Resize 方法动态扩容,每行对应表格记录,每列绑定一个编辑或标签对象实例。
重写 OnEvent 才能捕获图表上的拖拽与点击消息,把用户输入映射成进度值并触发重绘,否则原生编辑框不会自动变进度条外观。