DoEasy. 控件 (第 11 部分): WinForms 对象 群组,CheckedListBox WinForms 对象·进阶篇
(2/3)· 同一容器里塞进互不干扰的多组 RadioButton,靠对象群组索引就能省掉一堆嵌套面板
不少人在一个面板里摆了六七个单选按钮,点来点去发现全绑成一组,想拆出两组独立开关只能再套容器。其实给对象分配不同群组索引,同一容器内的子群组就能各玩各的,根本不用多建面板。
「账户对象构造时的属性快照」
在 MT5 的 EA 框架里,CAccount 类构造函数负责把账户关键状态一次性抓进本地数组,避免后续反复调用终端接口拖慢tick处理。 下面这段构造代码先设好整型与双精度属性的数组容量(ACCOUNT_PROP_INTEGER_TOTAL / ACCOUNT_PROP_DOUBLE_TOTAL),再 ResetChangesParams 和 ResetControlsParams 清空变更标记,相当于给账户对象做 baseline 初始化。 值得注意的两个兼容点:MQL5 下 ACCOUNT_MARGIN_MODE 与 ACCOUNT_CURRENCY_DIGITS 直接取 AccountInfoInteger,非 MQL5 环境则硬写对冲模式与 2 位小数;SERVER_TYPE 用 TerminalInfoString 找 "MetaTrader 5" 子串,命中返回 5 否则 4。 FIFO_CLOSE 在 MQL5 构建号低于 2155 时强制 false,≥2155 才读 ACCOUNT_FIFO_CLOSE——老构建不支持 FIFO 平仓标记。外汇和贵金属杠杆品种受 FIFO 规则影响平仓顺序,实盘前应在 MT5 按 Ctrl+D 看账户是否返回 true,避免回测逻辑和实盘不一致。
CAccount::CAccount(class="type">void) { this.m_type=OBJECT_DE_TYPE_ACCOUNT; class=class="str">"cmt">//--- Initialize control data this.SetControlDataArraySizeLong(ACCOUNT_PROP_INTEGER_TOTAL); this.SetControlDataArraySizeDouble(ACCOUNT_PROP_DOUBLE_TOTAL); this.ResetChangesParams(); this.ResetControlsParams(); class=class="str">"cmt">//--- Save integer properties this.m_long_prop[ACCOUNT_PROP_LOGIN] = ::AccountInfoInteger(ACCOUNT_LOGIN); this.m_long_prop[ACCOUNT_PROP_TRADE_MODE] = ::AccountInfoInteger(ACCOUNT_TRADE_MODE); this.m_long_prop[ACCOUNT_PROP_LEVERAGE] = ::AccountInfoInteger(ACCOUNT_LEVERAGE); this.m_long_prop[ACCOUNT_PROP_LIMIT_ORDERS] = ::AccountInfoInteger(ACCOUNT_LIMIT_ORDERS); this.m_long_prop[ACCOUNT_PROP_MARGIN_SO_MODE] = ::AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE); this.m_long_prop[ACCOUNT_PROP_TRADE_ALLOWED] = ::AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); this.m_long_prop[ACCOUNT_PROP_TRADE_EXPERT] = ::AccountInfoInteger(ACCOUNT_TRADE_EXPERT); this.m_long_prop[ACCOUNT_PROP_MARGIN_MODE] = class="macro">#ifdef __MQL5__ ::AccountInfoInteger(ACCOUNT_MARGIN_MODE) class="macro">#else ACCOUNT_MARGIN_MODE_RETAIL_HEDGING class="macro">#endif ; this.m_long_prop[ACCOUNT_PROP_CURRENCY_DIGITS] = class="macro">#ifdef __MQL5__ ::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) class="macro">#else class="num">2 class="macro">#endif ; this.m_long_prop[ACCOUNT_PROP_SERVER_TYPE] = (::StringFind(::TerminalInfoString(TERMINAL_NAME),"MetaTrader class="num">5")>WRONG_VALUE ? class="num">5 : class="num">4); this.m_long_prop[ACCOUNT_PROP_FIFO_CLOSE] = (class="macro">#ifdef __MQL5__ ::TerminalInfoInteger(TERMINAL_BUILD)<class="num">2155 ? false : ::AccountInfoInteger(ACCOUNT_FIFO_CLOSE) class="macro">#else false class="macro">#endif );
◍ 账户快照在类里的落地写法
封装账户状态时常把实时字段缓存进对象数组,避免每次调 AccountInfo 系列函数反复穿透终端接口。下面这段把对冲开关与一众双精度、字符串属性一次性写进成员变量,开 MT5 用 build 3245 以上版本跑才能拿到真实的 ACCOUNT_HEDGE_ALLOWED。 对冲许可的判断嵌了编译期分支:MQL5 环境下若终端 build 低于 3245 直接给 false,否则取 AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED);MQL4 分支恒为 false。这意味着老版本终端锁死不支持对冲,写 EA 前先确认 build 号,外汇和贵金属账户的对锁逻辑才可能生效。 双精度属性覆盖余额、信用、利润、净值、已用保证金、 free margin、保证金水平、止损调用线、止损平仓线、初始与维持保证金、资产、负债、冻结佣金,共 15 个字段。字符串侧存账户名、服务器、币种、券商公司名,方便后续面板直接读缓存而不打扰终端。
this.m_long_prop[ACCOUNT_PROP_HEDGE_ALLOWED] = (class="macro">#ifdef __MQL5__::TerminalInfoInteger(TERMINAL_BUILD)<class="num">3245 ? false : ::AccountInfoInteger(ACCOUNT_HEDGE_ALLOWED) class="macro">#else false class="macro">#endif ); class=class="str">"cmt">//--- Save real properties this.m_double_prop[this.IndexProp(ACCOUNT_PROP_BALANCE)] = ::AccountInfoDouble(ACCOUNT_BALANCE); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_CREDIT)] = ::AccountInfoDouble(ACCOUNT_CREDIT); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_PROFIT)] = ::AccountInfoDouble(ACCOUNT_PROFIT); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_EQUITY)] = ::AccountInfoDouble(ACCOUNT_EQUITY); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN)] = ::AccountInfoDouble(ACCOUNT_MARGIN); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_FREE)] = ::AccountInfoDouble(ACCOUNT_MARGIN_FREE); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_LEVEL)] = ::AccountInfoDouble(ACCOUNT_MARGIN_LEVEL); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_SO_CALL)] = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_CALL); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_SO_SO)] = ::AccountInfoDouble(ACCOUNT_MARGIN_SO_SO); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_INITIAL)] = ::AccountInfoDouble(ACCOUNT_MARGIN_INITIAL); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_MARGIN_MAINTENANCE)]=::AccountInfoDouble(ACCOUNT_MARGIN_MAINTENANCE); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_ASSETS)] = ::AccountInfoDouble(ACCOUNT_ASSETS); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_LIABILITIES)] = ::AccountInfoDouble(ACCOUNT_LIABILITIES); this.m_double_prop[this.IndexProp(ACCOUNT_PROP_COMMISSION_BLOCKED)]=::AccountInfoDouble(ACCOUNT_COMMISSION_BLOCKED); class=class="str">"cmt">//--- Save class="type">class="kw">string properties this.m_string_prop[this.IndexProp(ACCOUNT_PROP_NAME)] = ::AccountInfoString(ACCOUNT_NAME); this.m_string_prop[this.IndexProp(ACCOUNT_PROP_SERVER)] = ::AccountInfoString(ACCOUNT_SERVER); this.m_string_prop[this.IndexProp(ACCOUNT_PROP_CURRENCY)] = ::AccountInfoString(ACCOUNT_CURRENCY); this.m_string_prop[this.IndexProp(ACCOUNT_PROP_COMPANY)] = ::AccountInfoString(ACCOUNT_COMPANY); class=class="str">"cmt">//--- Account object name, object and account type(MetaTrader class="num">5 or class="num">4)
账户对象刷新与图形基类 setter 的内部写法
在 MT5 标准库扩展里,账户对象的 Refresh 动作并不是简单读一次终端属性。它先把当前登录号、账户名、经纪商公司名拼成展示串,再把服务端类型从 long 数组的 ACCOUNT_PROP_SERVER_TYPE 位强转成 uchar 存进 m_type_server,这一步决定了后续事件路由走实盘还是模拟分支。 紧接着用两个 for 循环把整数型和双精度型的账户属性快照塞进 _event 三维数组的第 4 列(下标 3),也就是把「刷新前」状态留底,方便 CBaseObjExt::Refresh() 里做差分比对。ACCOUNT_PROP_INTEGER_TOTAL 与 ACCOUNT_PROP_DOUBLE_TOTAL 是编译期常量,通常整数项在 20 个上下、双精度项在 10 个左右,循环成本可忽略。 下面的 setter 群基本都是单行赋值,但注意 SetGroup 被标了 virtual,说明派生图形对象可能重写分组逻辑;而 Zorder 用 const 修饰且返回 m_zorder,意味着图层叠放顺序在对象生命周期内只读。 把这段直接贴进你的 EA 调试工程,改一处:把 m_long_prop_event[i][3] 的下标 3 改成 2,再断点看事件比对是否错位——能立刻验证差分机制是不是依赖这个固定槽位。
this.m_name=CMessage::Text(MSG_LIB_PROP_ACCOUNT)+" "+(class="type">class="kw">string)this.Login()+": "+this.Name()+" ("+this.Company()+")"; this.m_type=COLLECTION_ACCOUNT_ID; this.m_type_server=(class="type">uchar)this.m_long_prop[ACCOUNT_PROP_SERVER_TYPE]; class=class="str">"cmt">//--- Filling in the current account data for(class="type">int i=class="num">0;i<ACCOUNT_PROP_INTEGER_TOTAL;i++) this.m_long_prop_event[i][class="num">3]=this.m_long_prop[i]; for(class="type">int i=class="num">0;i<ACCOUNT_PROP_DOUBLE_TOTAL;i++) this.m_double_prop_event[i][class="num">3]=this.m_double_prop[i]; class=class="str">"cmt">//--- Update the base object data and search for changes CBaseObjExt::Refresh(); } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//--- Set the values of the class variables class="type">void SetObjectID(class="kw">const class="type">long value) { this.m_object_id=value; } class="type">void SetBelong(class="kw">const ENUM_GRAPH_OBJ_BELONG belong){ this.m_belong=belong; } class="type">void SetTypeGraphObject(class="kw">const ENUM_OBJECT obj) { this.m_type_graph_obj=obj; } class="type">void SetTypeElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type) { this.m_type_element=type; } class="type">void SetSpecies(class="kw">const ENUM_GRAPH_OBJ_SPECIES species){ this.m_species=species; } class="kw">virtual class="type">void SetGroup(class="kw">const class="type">int value) { this.m_group=value; } class="type">void SetName(class="kw">const class="type">class="kw">string name) { this.m_name=name; } class="type">void SetDigits(class="kw">const class="type">int value) { this.m_digits=value; } class="kw">virtual class="type">long Zorder(class="type">void) class="kw">const { class="kw">return this.m_zorder; }
「图形对象属性的只读访问器」
在 MQL5 的 CWnd 派生体系里,对象的大部分外观与状态参数都通过 const 成员函数暴露,外部逻辑只能读不能改。下面这段声明集中给出了子窗口号、Y 偏移、可见周期掩码、报价小数位等基础取值入口。 其中 Group() 被标成 virtual,意味着子类可重写分组归属逻辑,而 IsMain() 不是存字段,而是拿 GetMain() 返回值与 NULL 比较——若返回 NULL 就说明自己是主对象。 IsBack / IsSelected / IsHidden / IsVisible 这类布尔接口直接返回内部标记位,实盘里可用它们判断鼠标选中状态或对象是否被脚本临时隐藏。外汇与贵金属杠杆高,用这些状态做自动面板时务必加防错判断,避免误读导致下单逻辑异常。 开 MT5 新建指标把这段声明塞进自定义类,编译后断点看 m_subwindow 与 m_timeframes_visible 的实际值,能快速验证你的对象挂在哪个子窗口、哪些周期可见。
class="type">int SubWindow(class="type">void) class="kw">const { class="kw">return this.m_subwindow; } class="type">int ShiftY(class="type">void) class="kw">const { class="kw">return this.m_shift_y; } class="type">int VisibleOnTimeframes(class="type">void) class="kw">const { class="kw">return this.m_timeframes_visible; } class="type">int Digits(class="type">void) class="kw">const { class="kw">return this.m_digits; } class="kw">virtual class="type">int Group(class="type">void) class="kw">const { class="kw">return this.m_group; } class="type">bool IsBack(class="type">void) class="kw">const { class="kw">return this.m_back; } class="type">bool IsSelected(class="type">void) class="kw">const { class="kw">return this.m_selected; } class="type">bool IsSelectable(class="type">void) class="kw">const { class="kw">return this.m_selectable; } class="type">bool IsHidden(class="type">void) class="kw">const { class="kw">return this.m_hidden; } class="type">bool IsVisible(class="type">void) class="kw">const { class="kw">return this.m_visible; } class=class="str">"cmt">//--- Return the flag indicating that the object is(class="num">1) main, (class="num">2) base class="type">bool IsMain(class="type">void) { class="kw">return this.GetMain()==NULL; }
◍ 画布元素的父子链与点击层级
在 MT5 自定义图形面板里,每个画布元素(CGCnvElement)都可能挂在一个主对象或基础对象之下。IsBase() 用 GetBase()==NULL 判断自身是否处于链条最底端,返回布尔值,调用开销极小,适合在事件循环里频繁跑。 GetMainID() 与 GetBaseID() 的逻辑对称:若自身就是主/基对象则直接返回 ID(),否则向上取指针再取 ID;取不到合法指针时回退到 WRONG_VALUE(通常定义为 -1)。这意味着当你在 EA 里遍历嵌套控件树时,可以用这两个方法 O(1) 定位根节点,不必手写递归。 Zorder 决定图表上图形对象接收鼠标点击事件的优先级。原类里 Zorder() 只读 CANV_ELEMENT_PROP_ZORDER 属性,SetZorder() 先调基类 CGBaseObj::SetZorder 失败就退出,成功才写本地属性——所以若你发现面板按钮点不动,先查基类返回 false 的原因(例如对象未绑定画布),而不是盲目改数值。外汇与贵金属图表叠加自定义画布属高风险操作,参数错乱可能遮挡报价区。 下面这段是原文核心方法,逐行拆一下: bool IsBase(void) { return this.GetBase()==NULL; } // 无参,若取不到父基对象即为基元 int GetBaseID(void) { if(this.IsBase()) return this.ID(); // 自己是基元,直接吐 ID CGCnvElement *base=this.GetBase(); // 向上拿基对象指针 return(base!=NULL ? base.ID() : WRONG_VALUE); // 空指针保护,否则回 WRONG_VALUE }
class="type">bool IsBase(class="type">void) { class="kw">return this.GetBase()==NULL; } class=class="str">"cmt">//--- Get the(class="num">1) main and(class="num">2) base object ID class="type">int GetMainID(class="type">void) { if(this.IsMain()) class="kw">return this.ID(); CGCnvElement *main=this.GetMain(); class="kw">return(main!=NULL ? main.ID() : WRONG_VALUE); } class="type">int GetBaseID(class="type">void) { if(this.IsBase()) class="kw">return this.ID(); CGCnvElement *base=this.GetBase(); class="kw">return(base!=NULL ? base.ID() : WRONG_VALUE); } class=class="str">"cmt">//--- Return the pointer to a canvas object class=class="str">"cmt">//--- Priority of a graphical object for receiving the event of clicking on a chart class="kw">virtual class="type">long Zorder(class="type">void) class="kw">const { class="kw">return this.GetProperty(CANV_ELEMENT_PROP_ZORDER); } class="kw">virtual class="type">bool SetZorder(class="kw">const class="type">long value,class="kw">const class="type">bool only_prop) { if(!CGBaseObj::SetZorder(value,only_prop)) class="kw">return false; this.SetProperty(CANV_ELEMENT_PROP_ZORDER,value); class="kw">return true;
表单对象的编组与层级取数接口
CForm 类在收尾部分把图形对象的归属关系做成了可读取的接口。Group() 直接返回 CANV_ELEMENT_PROP_GROUP 属性值,SetGroup() 先调基类 CGBaseObj::SetGroup 再写本地属性,意味着子对象编组会沿继承链同步,不会只改表层。 通过 GetObject、GetListElements、GetListInteractObj、GetShadowObj 四个指针返回方法,你能一次性拿到表单自身、挂载元素列表、可交互对象列表和阴影对象。写 EA 面板时若要对整组控件批量改坐标,直接遍历 GetListElements 比手动维护数组省事。 GetMaxLongPropForm / GetMaxIDForm 只扫绑定到基对象的直接层,而 GetMaxLongPropAll / GetMaxIDAll 会下钻整棵关联树。做动态 ID 分配时,用 All 系列可避免子表单里出现重复 ID,概率上能少踩重绘报错的坑。 外汇与贵金属图表上挂这类自定义表单属于高频重绘场景,实盘前务必在 MT5 策略测试器用历史 tick 跑一遍,确认对象数超过 200 时 Group 属性读写无明显延迟。
class="kw">virtual class="type">int Group(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_GROUP); } class="kw">virtual class="type">void SetGroup(class="kw">const class="type">int value) { CGBaseObj::SetGroup(value); this.SetProperty(CANV_ELEMENT_PROP_GROUP,value); } CForm *GetObject(class="type">void) { class="kw">return &this; } CArrayObj *GetListElements(class="type">void) { class="kw">return &this.m_list_elements; } CArrayObj *GetListInteractObj(class="type">void) { class="kw">return &this.m_list_interact; } CShadowObj *GetShadowObj(class="type">void) { class="kw">return this.m_shadow_obj; } class="type">long GetMaxLongPropForm(class="kw">const ENUM_CANV_ELEMENT_PROP_INTEGER prop); class="type">int GetMaxIDForm(class="type">void); class="type">long GetMaxLongPropAll(class="kw">const ENUM_CANV_ELEMENT_PROP_INTEGER prop); class="type">int GetMaxIDAll(class="type">void); obj.SetID(this.ID());