创建一个人工交易助手·进阶篇
(2/3)· 从功能清单到 CAppDialog 派生类,手把手把按钮、输入框和位图按钮落进真实控件
用 Create 画出趋势线对象的底层参数
在 MT5 的自定义图形对象类里,Create 方法决定了趋势线、通道等元素能否挂到指定图表上。它的参数顺序很死:先 chart ID,再对象 name,再 subwin 子窗口序号,最后两组坐标点 (x1,y1) 与 (x2,y2)。 很多人直接在主图(subwin=0)画,却忘了 name 必须全局唯一,否则后一次 Create 会静默覆盖前一条线。开 MT5 用脚本跑一下同名对象就能复现这个坑。 下面这段声明就是该类方法的原型,八个入参一个都不能省:
class="kw">virtual 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 subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2);
◍ 按类型分批初始化四组对象
Creat(...) 里要做的事很直接:把前面声明的全部对象逐一初始化。按前文统计,共声明了 4 类对象、合计 45 个,所以最清晰的做法是拆成 4 个私有过程,每类对象各走一个初始化流程,类内的初始化函数都放在 private 段。 有人会想把对象塞进数组统一管,但那样变量名就丢了和具体功能的语义绑定,后面调对象反而更绕。这里为了可读性和维护简单,刻意不用对象数组,45 个对象就老老实实按名字逐个 new。 你在 MT5 里写自己的 EA 时,如果对象数量上到几十个,也可以照这个思路分组初始化——先数清楚类型与总数,再决定要不要牺牲一点语法糖换代码透明度。外汇与贵金属品种波动剧烈,这类结构开销对实盘风控无直接影响,但乱了结构容易在紧急改参数时出错。
「面板文字标签的差异化与自动命名」
做交易面板时,CLabel 负责把信息文字画到图上。先分清哪些属性因元件而异:对象名、显示文本、坐标、相对锚点的对齐方式。其余统一的属性走函数参数,避免每个标签都手写一堆重复配置。 对象名必须全局唯一,这是 MT5 图表对象的硬约束。我倾向在程序里自动生成,而不是手动传字符串——用基类前缀拼上 'Label' 再加当前 OBJ_LABEL 总数,序列号自然不撞车。 文本、x/y 坐标和对齐方式用参数传入。对齐专门做了枚举 label_align:left=-1、right=1、center=0,读代码时一眼能懂。函数还收 chart、subwindow 和对象引用,方便挂到指定子窗口。 下面这段是实际创建逻辑。先拼名,再调父类 Create,失败直接返 false;接着写 Text,再用 ObjectSetInteger 按枚举映射 ANCHOR 常量;最后 Add 进容器。复制去 MT5 跑,注意 ObjectsTotal 的第三个参数用 OBJ_LABEL 才只数标签类。
class="type">class="kw">string name=m_name+"Label"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_LABEL); enum label_align { left=-class="num">1, right=class="num">1, center=class="num">0 }; class="type">bool CTradePanel::CreateLabel(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CLabel &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,label_align align) { class=class="str">"cmt">// 所有对象必须使用独立的名称 class="type">class="kw">string name=m_name+"Label"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_LABEL); class=class="str">"cmt">//--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,class="num">0,class="num">0)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 设置文字 if(!object.Text(text)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 把文字在对话框网格中对齐 ObjectSetInteger(chart,object.Name(),OBJPROP_ANCHOR,(align==left ? ANCHOR_LEFT_UPPER : (align==right ? ANCHOR_RIGHT_UPPER : ANCHOR_UPPER))); class=class="str">"cmt">//--- 把对象加到控件中 if(!Add(object)) { class="kw">return class="kw">false; } class="kw">return true; }
用 CButton 搭可点按钮的初始化坑点
CButton 用来画带文字的长方按钮,我们面板上的下单、平仓标准键就归它管。它继承自父类,文字默认居中,所以创建时不用再手动对齐,只要把按钮宽高通过参数传进去就行。 和纯标签不同,按钮有「按下 / 抬起」两种状态,被按下的还能锁死。实际面板里我们不希望按钮卡在锁定态,所以在初始化阶段统一关掉 Locking,并强制 Pressed(false) 回到抬起。 下面这段 CreateButton 是实打实能贴进 CTradePanel 类用的。名字用 m_name+"Button" 拼上当前 OBJ_BUTTON 总数,保证每个按钮独立不重名;若任意一步 Create / Text / Pressed / Add 返回 false,整个函数直接退回 false,调用方拿不到按钮对象就得查图表子窗口权限。 外汇与贵金属杠杆高,面板只是辅助发单,按钮逻辑出错可能漏单或重复发单,上 MT5 前先开策略测试器点一遍。
class="type">bool CTradePanel::CreateButton(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CButton &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="kw">const class="type">uint x_size,class="kw">const class="type">uint y_size) { class=class="str">"cmt">// 所有对象必须使用独立的名称 class="type">class="kw">string name=m_name+"Button"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_BUTTON); class=class="str">"cmt">//--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 设置文字 if(!object.Text(text)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 设置按钮的解锁标志 object.Locking(class="kw">false); class=class="str">"cmt">//--- 把按钮标志设为抬起 if(!object.Pressed(class="kw">false)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 把对象加到控件中 if(!Add(object)) { class="kw">return class="kw">false; } class="kw">return true; }
◍ 用 CEdit 搭可改写的输入格
在 MT5 面板里做交易量、止损止盈点数、风险百分比的录入框,CEdit 是最直接的控件类。它和按钮、标签的创建流程一致,但初始化时必须显式声明文字对齐方式,否则默认排布会让数字看起来歪斜难读。 要记住一点:CEdit 收发的一切内容都被当成字符串。你往框里塞 0.1 手或 50 点,得先走 DoubleToString 或 IntegerToString 转成文本,直接传数值类型会在编译或显示环节出问题。 按钮有按下抬起两种态,CEdit 没有,但它支持运行期锁成只读。实例里我们故意设 ReadOnly(false),让用户在面板跑起来后仍能改参数,这一步漏掉就会得到一排灰掉改不动的框。 下面这段 CreateEdit 是实际可用的封装:用 ObjectsTotal 拼唯一名避免重名,居中对齐,关掉只读,再挂进控件树。复制到 EA 的 panel 类里,把 m_name 换成你的前缀就能直接编译验证。
class="type">bool CTradePanel::CreateEdit(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CEdit &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="kw">const class="type">uint x_size,class="kw">const class="type">uint y_size) { class=class="str">"cmt">// 所有对象必须使用独立的名称 class="type">class="kw">string name=m_name+"Edit"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_EDIT); class=class="str">"cmt">//--- 调用创建函数 if(!object.Create(chart,name,subwindow,x,y,x+x_size,y+y_size)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 设置文字 if(!object.Text(text)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 在编辑框中对齐文字 if(!object.TextAlign(ALIGN_CENTER)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 把只读标志设为 class="kw">false if(!object.ReadOnly(class="kw">false)) { class="kw">return class="kw">false; } class=class="str">"cmt">//--- 把对象加到控件中 if(!Add(object)) { class="kw">return class="kw">false; } class="kw">return true; }
「用位图按钮替代标准控件」
做自定义交易面板时,标准按钮在视觉传达上往往不够直观。CBmpButton 类走的是图形对象路线,用 BMP 图片表达按下 / 抬起状态,而不是靠文字标签。在我们的面板里,它承担了三类交互:止损 / 获利 / 风险单位切换的单选(资金、点数或百分比)、控制止损获利水平显示的复选框,以及增减交易量的步进按钮。 和 CButton 相比,操作逻辑接近,核心差异在于状态切换传的是图形资源而非字符串。为让成品单文件发布,所有按钮图片都通过 #resource 预编译指令嵌进 exe,路径指向 Include\Controls\res 下的六张图(RadioButton 开 / 关、CheckBox 开 / 关、SpinInc、SpinDec)。 注意一个行为细节:除手数加减按钮外,其余元件都需保持按下或抬起的锁定状态,因此初始化函数要额外传入 lock 参数。创建函数本身必须声明在类的 private 段,外部只调封装好的 CreateBmpButton。 下面这段是实际建按钮的核心,坐标用 Y_STEP 和 CONTROLS_BUTTON_SIZE 推算,名称靠 ObjectsTotal 计数防重名: #resource "\\Include\\Controls\\res\\RadioButtonOn.bmp" #resource "\\Include\\Controls\\res\\RadioButtonOff.bmp" #resource "\\Include\\Controls\\res\\CheckBoxOn.bmp" #resource "\\Include\\Controls\\res\\CheckBoxOff.bmp" #resource "\\Include\\Controls\\res\\SpinInc.bmp" #resource "\\Include\\Controls\\res\\SpinDec.bmp" //+------------------------------------------------------------------+
| // | 创建图形(BMP)按钮 |
|---|
//+------------------------------------------------------------------+ bool CTradePanel::CreateBmpButton(const long chart,const int subwindow,CBmpButton &object,const uint x,const uint y,string BmpON,string BmpOFF,bool lock) { // 所有对象必须使用独立的名称 string name=m_name+"BmpButton"+(string)ObjectsTotal(chart,-1,OBJ_BITMAP_LABEL); //--- 计算坐标 uint y1=(uint)(y-(Y_STEP-CONTROLS_BUTTON_SIZE)/2); uint y2=y1+CONTROLS_BUTTON_SIZE; //--- 调用创建函数 if(!object.Create(m_chart_id,name,m_subwin,x-CONTROLS_BUTTON_SIZE,y1,x,y2)) return(false); //--- 把 BMP 图片复制到按钮状态 if(!object.BmpNames(BmpOFF,BmpON)) return(false); //--- 把对象加到控件中 if(!Add(object)) return(false); //--- 把锁定标志设为 true object.Locking(lock); //--- 成功 return(true); } private: //--- 创建标签对象 bool CreateLabel(const long chart,const int subwindow,CLabel &object,const string text,const uint x,const uint y,label_align align); //--- 创建按钮 bool CreateButton(const long chart,const int subwindow,CButton &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size); //--- 创建编辑框对象 bool CreateEdit(const long chart,const int subwindow,CEdit &object,const string text,const uint x,const uint y,const uint x_size,const uint y_size); //--- 创建图形按钮 逐行看:前六行 #resource 把六张位图编译进程序;CreateBmpButton 先用 ObjectsTotal 拼出唯一名,避免图表上重名冲突;y1/y2 按步进和尺寸反推命中区;object.Create 用位图标签画出来;BmpNames 绑定关 / 开两态图;Add 入控件树;Locking(lock) 决定它会不会记住被点下的状态。手数加减传 false,单选复选传 true,面板状态就不会乱跳。 开 MT5 把 res 目录放对位置直接编译,改 CONTROLS_BUTTON_SIZE 就能试不同点击热区;外汇贵金属面板涉及下单,实盘前务必在策略测试器跑通,这类自定义控件在高分屏 DPI 下可能偏位,属于已知需自查的风险点。
class="macro">#resource "\\Include\\Controls\\res\\RadioButtonOn.bmp" class="macro">#resource "\\Include\\Controls\\res\\RadioButtonOff.bmp" class="macro">#resource "\\Include\\Controls\\res\\CheckBoxOn.bmp" class="macro">#resource "\\Include\\Controls\\res\\CheckBoxOff.bmp" class="macro">#resource "\\Include\\Controls\\res\\SpinInc.bmp" class="macro">#resource "\\Include\\Controls\\res\\SpinDec.bmp" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建图形(BMP)按钮 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTradePanel::CreateBmpButton(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CBmpButton &object,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="type">class="kw">string BmpON,class="type">class="kw">string BmpOFF,class="type">bool lock) { class=class="str">"cmt">// 所有对象必须使用独立的名称 class="type">class="kw">string name=m_name+"BmpButton"+(class="type">class="kw">string)ObjectsTotal(chart,-class="num">1,OBJ_BITMAP_LABEL); class=class="str">"cmt">//--- 计算坐标 class="type">uint y1=(class="type">uint)(y-(Y_STEP-CONTROLS_BUTTON_SIZE)/class="num">2); class="type">uint y2=y1+CONTROLS_BUTTON_SIZE; class=class="str">"cmt">//--- 调用创建函数 if(!object.Create(m_chart_id,name,m_subwin,x-CONTROLS_BUTTON_SIZE,y1,x,y2)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 把 BMP 图片复制到按钮状态 if(!object.BmpNames(BmpOFF,BmpON)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 把对象加到控件中 if(!Add(object)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 把锁定标志设为 true object.Locking(lock); class=class="str">"cmt">//--- 成功 class="kw">return(true); } class="kw">private: class=class="str">"cmt">//--- 创建标签对象 class="type">bool CreateLabel(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CLabel &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,label_align align); class=class="str">"cmt">//--- 创建按钮 class="type">bool CreateButton(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CButton &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="kw">const class="type">uint x_size,class="kw">const class="type">uint y_size); class=class="str">"cmt">//--- 创建编辑框对象 class="type">bool CreateEdit(class="kw">const class="type">long chart,class="kw">const class="type">int subwindow,CEdit &object,class="kw">const class="type">class="kw">string text,class="kw">const class="type">uint x,class="kw">const class="type">uint y,class="kw">const class="type">uint x_size,class="kw">const class="type">uint y_size); class=class="str">"cmt">//--- 创建图形按钮