基于 CChartObject 类设计和实施新 GUI 组件·进阶篇
(2/3)· 接基础篇之后,深入 CChartObjectEdit 派生类的实施细节,解决复杂指标界面卡顿与难维护的痛点
◍ 用 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),也就是向右无限延伸射线——这通常用来标关键压力或支撑延长线。
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 到外层。
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。
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() 相加,就能看到实时联动。外汇与贵金属杠杆高、波动快,这类界面控件只解决输入效率,不暗示任何方向概率。
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 闭环通了。
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(); } }