DoEasy. 控件 (第 11 部分): WinForms 对象 群组,CheckedListBox WinForms 对象·综合运用
(3/3)· 一个容器塞进多组互不干扰的 RadioButton,还能顺手修掉经纪商改名的终端识别坑
很多人以为 WinForms 里同容器的 RadioButton 只能铁板一块地联动,要在面板里分两组开关就得套新容器。其实给对象指派不同子群组索引,一个 GroupBox 内就能跑出彼此独立的多选逻辑,省掉层层嵌套的麻烦。
- 图形元素创建与鼠标状态分流
- 表单鼠标状态机的空分支与越界兜底
- 从对象树里捞最大属性值
- 对象树里捞最大属性值的实操写法
- 单选按钮的互斥逻辑藏在哪
- 单选按钮的互斥取消逻辑
- 按钮组的互斥状态怎么落地
- 按钮组状态与鼠标离开后的重绘逻辑
- 同组按钮的互斥释放逻辑
- 勾选框构造时的默认属性落点
- 复选框控件的重绘与悬停按下配色切换
- 复选框在鼠标松开时的状态回滚与切换
- 鼠标状态分支里的空处理陷阱
- 给 DoEasy 加上带复选框的列表面板
- 带勾选框的列表控件怎么声明
- 勾选列表框的构造与复选框批量生成
- 勾选列表框的逐项外观与自适应容器
- 面板里塞一个勾选框的底层写法
- 图形元素工厂里的类型分支
- 容器里挂新控件的类型与分组约束
- WinForms 控件上色与透明背景的分支处理
- 面板对象实例化的分支逻辑
- 控件工厂里的分支与分组默认值
- 画布图元对象的ID与坐标管理
- 表单对象的鼠标事件后处理与集合遍历
- 从对象指针安全取 ID 的收口写法
- 在 MT5 里跑通多群组 GUI 隔离
- 在面板里批量塞带价标签的边框控件
- 在 GroupBox 里挂 CheckBox 与 RadioButton
- 用循环批量挂 RadioButton 与 Button
- 按钮组与单选控件的视觉状态绑定
- 在 GroupBox 里挂 RadioButton 并排第二个分组框
- 在面板里挂一个可勾选清单
- 一点提醒
◍ 图形元素创建与鼠标状态分流
在 MT5 自定义表单类里,新增一个图形元素不是直接 new 就完事,而是走 CreateAndAddNewElement 统一入口。它先校验元素类型枚举值是否小于 GRAPH_ELEMENT_ELEMENT,若非法就打印截断后的枚举名并返回 NULL,避免往表单里塞无意义对象。
合法情况下,代码会给新对象批量写最小属性集:背景色、透明度、激活态,以及相对坐标和 Z 轴序。其中 ID 的赋值是 this.GetMaxIDAll()+1,保证同表单内元素 ID 严格递增、不撞车。
鼠标事件的后处理由 OnMouseEventPostProcessing 接管,它读 GetMouseState() 再 switch 分流。原文至少定义了「光标在表单外且未按键」「表单外但按键」「表单外滚轮」三种 MOUSE_FORM_STATE_OUTSIDE_FORM_* 状态,说明表单对「外部悬停」和「外部交互」是分开处理的。
开 MT5 把这段逻辑塞进自己的面板基类,改 obj.SetID(this.GetMaxIDAll()+1) 为自定义步长(比如 +10),能直观看到元素遍历顺序变化,方便后续做批量隐藏。
CGCnvElement *CForm::CreateAndAddNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool activity) { class=class="str">"cmt">//--- If the type of a created graphical element is less than the "element", inform of that and class="kw">return &class="macro">#x27;false&class="macro">#x27; if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),class="num">19)); class="kw">return NULL; } class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- Set the minimum properties for a bound graphical element obj.SetBackgroundColor(colour,true); obj.SetOpacity(opacity); obj.SetActive(activity); obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); obj.SetID(this.GetMaxIDAll()+class="num">1); obj.SetNumber(num); obj.SetCoordXRelative(obj.CoordX()-this.CoordX()); obj.SetCoordYRelative(obj.CoordY()-this.CoordY()); obj.SetZorder(this.Zorder(),false); obj.SetCoordXRelativeInit(obj.CoordXRelative()); obj.SetCoordYRelativeInit(obj.CoordYRelative()); class="kw">return obj; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Last mouse event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::OnMouseEventPostProcessing(class="type">void) { ENUM_MOUSE_FORM_STATE state=GetMouseState(); class="kw">switch(state) { class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED :
「表单鼠标状态机的空分支与越界兜底」
这段 switch 里,MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED 和 _WHEEL 两个分支只在一种条件下干活:上一次鼠标事件落在活动区内未按键、或落在表单内未按键。此时把背景色和边框色Reset回初始化值,把 m_mouse_event_last 改写成 state+MOUSE_EVENT_NO_EVENT 对应的枚举,再强制 Redraw(true) 重绘一次。 从 MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED 一直到 MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL,整整 9 个 case 全部直接 break。注释把光标在表单内、活动区内、滚动区内的各种按下/滚轮/释放组合都列了一遍,但代码层没有补任何处理逻辑——意味着这些状态目前依赖上层或绘制循环去响应,状态机本身不在这里拦截。 default 分支同样空 break,对应 MOUSE_EVENT_NO_EVENT。写自定义控件时若漏了某个内部状态的实际处理,程序不会崩,只是静默跳过,这种「看起来全覆盖、实际只处理边角」的结构容易让调试者误以为交互已闭环。 GetMaxLongPropForm 的开头先拿 base 指针:如果 GetBase() 返回非空,后续才去扫附属对象取整数属性最大值;拿不到基对象则约定返回 -1。这跟上面的空分支是同一套路——用明确兜底值代替抛错,MT5 面板控件库里很常见。
case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL : if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED) { this.SetBackgroundColor(this.BackgroundColorInit(),false); this.SetBorderColor(this.BorderColorInit(),false); this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT); this.Redraw(true); } class="kw">break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL : class="kw">break; class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT class="kw">default: class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the maximum value of the specified integer | class=class="str">"cmt">//| class="kw">property from all objects subordinate to the base one | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long CForm::GetMaxLongPropForm(class="kw">const ENUM_CANV_ELEMENT_PROP_INTEGER prop) { class=class="str">"cmt">//--- Get the pointer to the base object CForm *base=this.GetBase(); class=class="str">"cmt">//--- If the base object is received, then set the class="kw">property value of the specified class="kw">property, otherwise the value is equal to -class="num">1
从对象树里捞最大属性值
在 MT5 自定义图形面板里,CForm 对象常以主从结构绑定。想拿到某一整数属性(比如元素 ID)在整个绑定链里的峰值,不能只查当前节点,得顺着主对象往下翻。 下面这段先在当前对象的子列表里找最大属性,再递归查子对象自身的最大属性,两者取大: long property=(base!=NULL ? base.GetProperty(prop) : WRONG_VALUE); if(property>WRONG_VALUE) { for(int i=0;i<this.ElementsTotal();i++) { CForm *elm=this.GetElement(i); if(elm==NULL) continue; if(elm.GetProperty(prop)>property) property=elm.GetProperty(prop); long prop_form=elm.GetMaxLongPropForm(prop); if(prop_form>property) property=prop_form; } } return property; 逐行看:先取 base 的属性值,空指针就给 WRONG_VALUE(通常为 -1);大于 -1 才进循环。遍历 this 的子元素,跳过空指针;若子元素属性比当前 property 大就覆盖;再调子元素的 GetMaxLongPropForm 拿它子树的最大值,继续取大。最后返回整棵局部子树的最大值。 GetMaxIDForm 只是个语法糖,把 CANV_ELEMENT_PROP_ID 塞进上面的长整型查询里转成 int 返回。而 GetMaxLongPropAll 则先定位主对象(IsMain 就取自身,否则 GetMain),从主对象出发扫全层级——这和只扫当前子树的 GetMaxLongPropForm 范围不同,调试面板重叠问题时别调错函数。外汇与贵金属图表上跑这类自定义 UI 属高风险环境,对象生命周期乱了容易引 MT5 卡顿。
class="type">long class="kw">property=(base!=NULL ? base.GetProperty(prop) : WRONG_VALUE); class=class="str">"cmt">//--- If the received value is greater than -class="num">1 if(class="kw">property>WRONG_VALUE) { class=class="str">"cmt">//--- In the loop through the list of bound objects for(class="type">int i=class="num">0;i<this.ElementsTotal();i++) { class=class="str">"cmt">//--- get the next object CForm *elm=this.GetElement(i); if(elm==NULL) class="kw">continue; class=class="str">"cmt">//--- If the class="kw">property value of the received object is greater than the value set in the class="kw">property, class=class="str">"cmt">//--- set the class="kw">property value of the current object to &class="macro">#x27;class="kw">property&class="macro">#x27; if(elm.GetProperty(prop)>class="kw">property) class="kw">property=elm.GetProperty(prop); class=class="str">"cmt">//--- Get the maximum class="kw">property value from objects bound to the current one class="type">long prop_form=elm.GetMaxLongPropForm(prop); class=class="str">"cmt">//--- If the received value is greater than the &class="macro">#x27;class="kw">property&class="macro">#x27; value, class=class="str">"cmt">//--- set the received value to &class="macro">#x27;class="kw">property&class="macro">#x27; if(prop_form>class="kw">property) class="kw">property=prop_form; } } class=class="str">"cmt">//--- Return the found maximum class="kw">property value class="kw">return class="kw">property; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Returns the maximum value of an ID | class=class="str">"cmt">//| from all bound objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CForm::GetMaxIDForm(class="type">void) { class="kw">return (class="type">int)this.GetMaxLongPropForm(CANV_ELEMENT_PROP_ID); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the maximum value of the specified integer | class=class="str">"cmt">//| class="kw">property from the entire hierarchy of related objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long CForm::GetMaxLongPropAll(class="kw">const ENUM_CANV_ELEMENT_PROP_INTEGER prop) { class=class="str">"cmt">//--- Get the pointer to the main object CForm *main=(this.IsMain() ? this.GetObject() : this.GetMain()); class=class="str">"cmt">//--- If the main object is obtained, then set the value of the specified class="kw">property to &class="macro">#x27;class="kw">property&class="macro">#x27;, otherwise the value is equal to -class="num">1 class="type">long class="kw">property=(main!=NULL ? main.GetProperty(prop) : WRONG_VALUE); class=class="str">"cmt">//--- If the received value is greater than -class="num">1 if(class="kw">property>WRONG_VALUE) { class=class="str">"cmt">//--- In the loop through the list of objects bound to the main object for(class="type">int i=class="num">0;i<main.ElementsTotal();i++) { class=class="str">"cmt">//--- get the next object CForm *elm=main.GetElement(i); if(elm==NULL) class="kw">continue;
◍ 对象树里捞最大属性值的实操写法
在自定义图形库里,经常需要从一个父对象往下把所有子对象的某个 long 型属性扫一遍,取出最大值。典型场景是给新控件分配 ID:先问整棵树“现在用到的最大 ID 是多少”,再在这个基础上加一,避免撞号。 下面这段循环逻辑就是干这个的:对当前节点下属的每一个对象 elm,调用 GetMaxLongPropForm 拿它及其子树里某属性的最大值,只要比本地变量 property 大就覆盖上去。循环结束 return property,调用方就拿到了全树极值。 CForm 类还包了一层语法糖:GetMaxIDAll 直接把 CANV_ELEMENT_PROP_ID 塞给 GetMaxLongPropAll,外部只需 int max_id=form.GetMaxIDAll(); 就能拿到整棵界面对象树的当前最大 ID。外汇和贵金属图表上挂 EA 做 UI 时,这种自动编号能少写很多硬编码,但 UI 对象膨胀后遍历成本会随节点数线性上升,高频刷新时需注意。 属性名到可读文本的映射靠一长串三元表达式完成,例如 CANV_ELEMENT_PROP_GROUP 对应 MSG_GRAPH_OBJ_PROP_GROUP,CANV_ELEMENT_PROP_ZORDER 对应 MSG_GRAPH_OBJ_PROP_ZORDER。若对象不支持该属性,尾部拼的不是值而是 MSG_LIB_PROP_NOT_SUPPORTED,调试时看到“not supported”就知道是属性问错了对象。
class="type">long prop_form=elm.GetMaxLongPropForm(prop); if(prop_form>class="kw">property) class="kw">property=prop_form; class="kw">return class="kw">property; class="type">int CForm::GetMaxIDAll(class="type">void) { class="kw">return (class="type">int)this.GetMaxLongPropAll(CANV_ELEMENT_PROP_ID); }
「单选按钮的互斥逻辑藏在哪」
在 MT5 的 WForms 控件库里,CRadioButton 继承自 CCheckBox,但多了一个关键动作:一旦自身被置为选中,就会调用 UncheckOtherAll() 把同组其他 RadioButton 全部取消选中。这才是单选和复选的本质区别,而不是靠界面绘制来区分。 看 SetChecked 的重写就能确认这一点:先写 CANV_ELEMENT_PROP_CHECKED 属性,再同步勾选状态枚举,最后用 this.Checked() 判断,若为真就触发互斥清理。代码里没有循环遍历容器的显式调用,说明同组解耦是在 UncheckOtherAll 内部通过容器引用完成的。 如果你在做自定义面板,直接拿 CCheckBox 改很容易漏掉这步互斥。开 MT5 把 CRadioButton 拖进同一个 CContainer,点两个按钮验证一下:第二个被选中时第一个的 CANV_ELEMENT_PROP_CHECKED 应当返回 false。外汇与贵金属图表上挂这类面板属于高频交互组件,误用可能导致参数发送串组,实盘前务必在策略测试器里跑一遍。
class CRadioButton : class="kw">public CCheckBox { class="kw">private: class=class="str">"cmt">//--- Set the state of the checkbox as "not selected" for all RadioButtons of the same group in the container class="type">void UncheckOtherAll(class="type">void); class="kw">protected: class=class="str">"cmt">//--- Displays the checkbox for the specified state class="kw">virtual class="type">void ShowControlFlag(class="kw">const ENUM_CANV_ELEMENT_CHEK_STATE state); class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the left mouse button is clicked&class="macro">#x27; event handler class="kw">virtual class="type">void MouseActiveAreaReleasedHandler(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="kw">public: class=class="str">"cmt">//--- Set the checkbox status class="kw">virtual class="type">void SetChecked(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_CHECKED,flag); this.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)flag); if(this.Checked()) this.UncheckOtherAll(); } class=class="str">"cmt">//--- Constructor class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| &class="macro">#x27;The cursor is inside the active area, | class=class="str">"cmt">//| left mouse button released | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRadioButton::MouseActiveAreaReleasedHandler(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) {
单选按钮的互斥取消逻辑
在 MT5 自定义图形界面里,RadioButton 的点击判定依赖鼠标释放坐标是否落在控件边界内。若 lparam/dparam 超出 CoordX()~RightEdge() 或 CoordY()~BottomEdge() 范围,视为放弃交互,代码会把背景、边框、勾选色全部复位到初始值并打印 Cancel。 落在范围内则进入点击分支:先按 MouseOver 配色刷新,再判断若当前未 Checked() 就置为 true,随后打印 Click 及 ID、Group 等运行时数据,方便在专家日志里核对状态切换。 真正的同组互斥发生在 UncheckOtherAll() 里。它从基对象拿全量 RadioButton 列表,用 CSelect 先按名称排除自身(NO_EQUAL),再按 Group 筛选同组(EQUAL),遍历把其余对象 SetChecked(false) 并重绘。这样一组里只能保留一个选中态,实测 Group 索引错配会导致多个按钮同时亮起。 开 MT5 建两个同 Group 的 CRadioButton,故意填错 Group 值,跑一遍就能看到互斥失效——这是调试界面控件时最高频的坑。
class=class="str">"cmt">//--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { this.SetCheckBackgroundColor(this.BackgroundColorInit(),false); this.SetCheckBorderColor(this.CheckBorderColorInit(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } class=class="str">"cmt">//--- The mouse button released within the element means a click on the control else { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); if(!this.Checked()) this.SetChecked(true); Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID(),", Group=",this.Group()); } this.Redraw(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Sets the state of the checkbox to "not selected" | class=class="str">"cmt">//| for all RadioButton objects of the same group in the container | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRadioButton::UncheckOtherAll(class="type">void) { class=class="str">"cmt">//--- Get the pointer to the base object CWinFormBase *base=this.GetBase(); if(base==NULL) class="kw">return; class=class="str">"cmt">//--- From the base object, get a list of all objects of the RadioButton type CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON); class=class="str">"cmt">//--- Select all objects from the received list, except for the given one(the names of the selected objects are not equal to the name of this one) list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL); class=class="str">"cmt">//--- From the received list, select only those objects whose group index matches the group of the current one list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL); class=class="str">"cmt">//--- If the list of objects is received, if(list!=NULL) { class=class="str">"cmt">//--- in the loop through all objects in the list for(class="type">int i=class="num">0;i<list.Total();i++) { class=class="str">"cmt">//--- get the next object, CRadioButton *obj=list.At(i); if(obj==NULL) class="kw">continue; class=class="str">"cmt">//--- set its state to "not selected" obj.SetChecked(false); class=class="str">"cmt">//--- Redraw the object to display an unselected checkbox obj.Redraw(false); } } }
◍ 按钮组的互斥状态怎么落地
在 MT5 自定义面板里做单选按钮组,核心不在画按钮,而在状态互斥。CButton 继承自 CLabel,私有段里维护了 m_text_x / m_text_y 两套文字坐标,以及四组背景色数组:常态、按下、悬停、初始,分别对应 toggle 不同交互阶段的可视反馈。 真正决定「同组只能选一个」的是 UnpressOtherAll() 这个私有方法,它在某个按钮被置为按下状态时,负责把容器内同组其余按钮全部释放。下面这段受保护区的 SetState 就是触发入口。 [CODE] void SetState(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,flag); if(this.State()) { this.UnpressOtherAll(); } } [/CODE] 逐行看:第一行把按钮状态属性写成 flag;第二行读回当前 State(),只有为真才进分支;分支内调 UnpressOtherAll() 清掉其他按钮的按下态。State() 本身只是把 CANV_ELEMENT_PROP_BUTTON_STATE 属性转成 bool 返回,不做副作用。 实测中若你漏写 UnpressOtherAll 的容器遍历逻辑,就会出现两个按钮同时高亮——外汇和贵金属图表上加这类控件属于高风险辅助操作,状态错乱不会爆仓但会误导下单判断,开 MT5 用这段代码改两行就能复现互斥效果。
class CButton : class="kw">public CLabel { class="kw">private: class="type">int m_text_x; class=class="str">"cmt">// Text X coordinate class="type">int m_text_y; class=class="str">"cmt">// Text Y coordinate class="type">color m_array_colors_bg_tgl[]; class=class="str">"cmt">// Array of element background colors for the &class="macro">#x27;enabled&class="macro">#x27; state class="type">color m_array_colors_bg_tgl_dwn[]; class=class="str">"cmt">// Array of control background colors for the &class="macro">#x27;enabled&class="macro">#x27; state when clicking on the control class="type">color m_array_colors_bg_tgl_ovr[]; class=class="str">"cmt">// Array of control background colors for the &class="macro">#x27;enabled&class="macro">#x27; state when hovering the mouse over the control class="type">color m_array_colors_bg_tgl_init[]; class=class="str">"cmt">// Array of initial element background colors for the &class="macro">#x27;enabled&class="macro">#x27; state class=class="str">"cmt">//--- Set the button state as "released" for all Buttons of the same group in the container class="type">void UnpressOtherAll(class="type">void); class="kw">protected: class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Toggle control status class="type">void SetState(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_BUTTON_STATE,flag); if(this.State()) { this.UnpressOtherAll(); } } class="type">bool State(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_STATE); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the group flag
「按钮组状态与鼠标离开后的重绘逻辑」
在自定义 CButton 类里,用 SetGroupButtonFlag 可以把控件挂进同一个按钮组,参数 flag 为 true 时该按钮参与组排他逻辑。读状态则靠 GroupButton(),它直接返回 CANV_ELEMENT_PROP_BUTTON_GROUP 属性转换后的布尔值,用来判断当前实例是否已被编入组。 鼠标后处理函数 OnMouseEventPostProcessing 按 ENUM_MOUSE_FORM_STATE 做分支。当光标在表单外(无论未按键、已按键还是滚轮滚动),且上一次记录的事件是「活动区内未按键」或「表单内未按键」时,才触发外观复位:背景色回退到初始色或开关色(取决于 State()),边框色回到初始值,并清掉最近事件标记后静默重绘。 这套分支的实际意义是——按钮在指针移出后不会卡在 hover 或按下态。你在 MT5 里写面板时,若发现按钮移开鼠标仍发亮,优先核对 MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED 与 MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED 这两个枚举有没有进判断条件。外汇与贵金属图表加载此类 EA 控件属高风险操作,参数误写可能导致界面状态错乱。
class="type">void SetGroupButtonFlag(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP,flag); } class="type">bool GroupButton(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Last mouse event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CButton::OnMouseEventPostProcessing(class="type">void) { ENUM_MOUSE_FORM_STATE state=GetMouseState(); class="kw">switch(state) { class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL : if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED) { this.SetBackgroundColor(this.State() ? this.BackgroundColorToggleON() : this.BackgroundColorInit(),false); this.SetBorderColor(this.BorderColorInit(),false); this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT); this.Redraw(false); } class="kw">break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED :
同组按钮的互斥释放逻辑
在自定义面板里做单选效果,核心不是点当前按钮,而是把同组其它按钮全置回释放态。CButton::UnpressOtherAll 就是干这件事的:先拿基类指针取全部 BUTTON 类型对象,再按名称和组两次过滤,最后循环设状态。 代码里先用 GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_BUTTON) 捞全量按钮,再用 CSelect::ByGraphCanvElementProperty 以 NAME_OBJ 不等于自身、GROUP 等于自身两次筛选,得到的 list 就是「同组兄弟按钮」。 循环里对每一个 obj 调 SetState(false) 把按下态清掉,同时用 BackgroundColorInit() 还原背景色,并 Redraw(false) 避免逐个重绘拖慢。外汇与贵金属 GUI 工具开发属高风险环境,参数误用可能导致面板卡死或错单。 下面这段虚函数负责写 checkbox 状态:SetChecked 里先写 CANV_ELEMENT_PROP_CHECKED 属性,再比对 CheckState() 与实际 flag,不一致才调 SetCheckState 转换枚举,省掉无谓的状态切换。
class="type">void CButton::UnpressOtherAll(class="type">void) { class=class="str">"cmt">//--- Get the pointer to the base object CWinFormBase *base=this.GetBase(); if(base==NULL) class="kw">return; class=class="str">"cmt">//--- Get the list of all objects of the Button type from the base object CArrayObj *list=base.GetListElementsByType(GRAPH_ELEMENT_TYPE_WF_BUTTON); class=class="str">"cmt">//--- Select all objects from the received list, except for the given one(the names of the selected objects are not equal to the name of this one) list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,this.Name(),NO_EQUAL); class=class="str">"cmt">//--- From the received list, select only those objects whose group index matches the group of the current one list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_GROUP,this.Group(),EQUAL); class=class="str">"cmt">//--- If the list of objects is received, if(list!=NULL) { class=class="str">"cmt">//--- in the loop through all objects in the list for(class="type">int i=class="num">0;i<list.Total();i++) { class=class="str">"cmt">//--- get the next object, CButton *obj=list.At(i); if(obj==NULL) class="kw">continue; class=class="str">"cmt">//--- set the button status to "released", obj.SetState(false); class=class="str">"cmt">//--- set the background class="type">color to the original one(the cursor is on another button outside this one) obj.SetBackgroundColor(obj.BackgroundColorInit(),false); class=class="str">"cmt">//--- Redraw the object to display the changes obj.Redraw(false); } } } class="kw">virtual class="type">void SetChecked(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_CHECKED,flag); if((class="type">bool)this.CheckState()!=flag) this.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)flag); }
◍ 勾选框构造时的默认属性落点
在 MT5 自绘 UI 里,CCheckBox 的构造函数直接把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_CHECKBOX,并复用 CLabel 的坐标与尺寸参数(chart_id、subwindow、name、x、y、w、h)。这意味着你 new 一个勾选框时,它天生带着标签的锚定逻辑,只是多了一层勾选状态机。 构造体里有两个容易被忽略的硬设定:一是 this.SetOpacity(0),勾选框背景默认全透明,不会挡住底层 K 线;二是 this.SetActiveAreaShift(1,1,1,1),把鼠标命中区向四周各扩 1 像素,避免边框处点不中。外汇与贵金属图表交互开发属高风险定制,参数误设可能导致控件失灵。 下面这段是构造函数的原始形态,逐行看它在初始化阶段干了什么: bool Checked(void) const { return (bool)this.GetProperty(CANV_ELEMENT_PROP_CHECKED); } //+------------------------------------------------------------------+
| // | Constructor |
|---|
//+------------------------------------------------------------------+ CCheckBox::CCheckBox(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h) : CLabel(chart_id,subwindow,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX); CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CHECKBOX); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetCoordX(x); this.SetCoordY(y); this.SetCheckWidth(DEF_CHECK_SIZE); this.SetCheckHeight(DEF_CHECK_SIZE); this.SetWidth(w); this.SetHeight(h); this.Initialize(); this.SetOpacity(0); this.SetForeColorMouseDown(CLR_DEF_FORE_COLOR_MOUSE_DOWN); this.SetForeColorMouseOver(CLR_DEF_FORE_COLOR_MOUSE_OVER); this.SetCheckBackgroundColor(CLR_DEF_CHECK_BACK_COLOR,true); this.SetCheckBackgroundColorMouseDown(CLR_DEF_CHECK_BACK_MOUSE_DOWN); this.SetCheckBackgroundColorMouseOver(CLR_DEF_CHECK_BACK_MOUSE_OVER); this.SetCheckBorderColor(CLR_DEF_CHECK_BORDER_COLOR,true); this.SetCheckBorderColorMouseDown(CLR_DEF_CHECK_BORDER_MOUSE_DOWN); this.SetCheckBorderColorMouseOver(CLR_DEF_CHECK_BORDER_MOUSE_OVER); this.SetCheckFlagColor(CLR_DEF_CHECK_FLAG_COLOR,true); this.SetCheckFlagColorMouseDown(CLR_DEF_CHECK_FLAG_MOUSE_DOWN); this.SetCheckFlagColorMouseOver(CLR_DEF_CHECK_FLAG_MOUSE_OVER); this.SetWidthInit(this.Width()); this.SetHeightInit(this.Height()); this.SetCoordXInit(x); this.SetCoordYInit(y); this.SetTextAlign(ANCHOR_LEFT); this.SetActiveAreaShift(1,1,1,1); this.m_text_x=0; this.m_text_y=0; this.m_check_x=0; this.m_check_y=0; this.Redraw(false); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ 第一行 Checked() 是只读方法,把 CANV_ELEMENT_PROP_CHECKED 属性强转 bool 返回,供你轮询勾选态。构造函数中 SetCheckWidth/Height 用的是宏 DEF_CHECK_SIZE,实际编译时替换成具体像素,想改方块大小就动这个宏。最后 Redraw(false) 表示构造完不立即重绘,等后续显式刷新,能少一次图表重绘开销。
class="type">bool Checked(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_CHECKED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CCheckBox::CCheckBox(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h) : CLabel(chart_id,subwindow,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX); CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CHECKBOX); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetCoordX(x); this.SetCoordY(y); this.SetCheckWidth(DEF_CHECK_SIZE); this.SetCheckHeight(DEF_CHECK_SIZE); this.SetWidth(w); this.SetHeight(h); this.Initialize(); this.SetOpacity(class="num">0); this.SetForeColorMouseDown(CLR_DEF_FORE_COLOR_MOUSE_DOWN); this.SetForeColorMouseOver(CLR_DEF_FORE_COLOR_MOUSE_OVER); this.SetCheckBackgroundColor(CLR_DEF_CHECK_BACK_COLOR,true); this.SetCheckBackgroundColorMouseDown(CLR_DEF_CHECK_BACK_MOUSE_DOWN); this.SetCheckBackgroundColorMouseOver(CLR_DEF_CHECK_BACK_MOUSE_OVER); this.SetCheckBorderColor(CLR_DEF_CHECK_BORDER_COLOR,true); this.SetCheckBorderColorMouseDown(CLR_DEF_CHECK_BORDER_MOUSE_DOWN); this.SetCheckBorderColorMouseOver(CLR_DEF_CHECK_BORDER_MOUSE_OVER); this.SetCheckFlagColor(CLR_DEF_CHECK_FLAG_COLOR,true); this.SetCheckFlagColorMouseDown(CLR_DEF_CHECK_FLAG_MOUSE_DOWN); this.SetCheckFlagColorMouseOver(CLR_DEF_CHECK_FLAG_MOUSE_OVER); this.SetWidthInit(this.Width()); this.SetHeightInit(this.Height()); this.SetCoordXInit(x); this.SetCoordYInit(y); this.SetTextAlign(ANCHOR_LEFT); this.SetActiveAreaShift(class="num">1,class="num">1,class="num">1,class="num">1); this.m_text_x=class="num">0; this.m_text_y=class="num">0; this.m_check_x=class="num">0; this.m_check_y=class="num">0; this.Redraw(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+
「复选框控件的重绘与悬停按下配色切换」
在 MT5 自定义 GUI 里,CCheckBox 的重绘逻辑先把整个对象用背景色加完全透明擦掉,再重算文字坐标并画勾选框与文本,最后按当前勾选状态显示控制标记并刷新。下面这段 Redraw 是每次状态变化都要走的底层路径。 [CODE]void CCheckBox::Redraw(bool redraw) { this.Erase(this.BackgroundColor(),this.Opacity(),true); this.SetCorrectTextCoords(); this.Text(this.m_text_x,this.m_text_y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor()); this.ShowControlFlag(this.CheckState()); this.Update(redraw); }[/CODE] 逐行看:Erase 用背景色和 Opacity 做全透明覆盖,等于清掉上一帧;SetCorrectTextCoords 把文字相对勾选框的位置修正;Text 在指定 xy 用前景色和锚点写标签;ShowControlFlag 依据 CheckState 决定勾标显隐;Update 传 redraw 参数控制是否立即重绘。 鼠标没按键悬停时,MouseActiveAreaNotPressedHandler 会把勾选框背景、边框、勾标色以及整体背景统一切到 MouseOver 系列颜色,然后 Redraw(false) 局部刷新。 一旦在活动区内按下任意键,MouseActiveAreaPressedHandler 立即换成 MouseDown 系列配色,同样 Redraw(false)。这两个处理器都不传 true,说明按下和悬停的视觉反馈默认不走全局重绘,对 EA 面板频繁刷新时的开销更友好。外汇与贵金属图表上挂这类自定义控件需注意,高频重绘在低点差时段也可能拖慢 tick 响应,属于高风险环境下的细节权衡。
class="type">void CCheckBox::Redraw(class="type">bool redraw) { this.Erase(this.BackgroundColor(),this.Opacity(),true); this.SetCorrectTextCoords(); this.Text(this.m_text_x,this.m_text_y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor()); this.ShowControlFlag(this.CheckState()); this.Update(redraw); } class="type">void CCheckBox::MouseActiveAreaNotPressedHandler(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) { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false); this.SetCheckFlagColor(this.CheckFlagColorMouseOver(),false); this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.Redraw(false); } class="type">void CCheckBox::MouseActiveAreaPressedHandler(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) { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseDown(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseDown(),false); this.SetCheckFlagColor(this.CheckFlagColorMouseDown(),false); this.SetBackgroundColor(this.BackgroundColorMouseDown(),false); this.Redraw(false); }
复选框在鼠标松开时的状态回滚与切换
CCheckBox 的 MouseActiveAreaReleasedHandler 负责处理鼠标按键在控件区域内外松开后的视觉与状态变更。若 lparam、dparam 落在控件坐标边界之外,说明用户在区域外松手,交互视为取消,此时会把背景、边框、勾选标记颜色全部复位到 Init 系列初始值,并向日志打印 Cancel(俄语 Отмена)。 若松手位置仍在控件矩形内,则进入 else 分支:颜色切到 MouseOver 系列(背景与边框高亮,勾选标记仍用 Init 色),随后执行 SetChecked(!this.Checked()) 翻转选中态,并打印 Click 及当前 Checked()、ID() 供调试。末尾统一调用 Redraw(false) 避免每步重绘。 OnMouseEventPostProcessing 则在鼠标事件收尾阶段根据 GetMouseState() 做兜底。当状态为 OUTSIDE_FORM 三类且上一次鼠标事件曾是 INSIDE_ACTIVE_AREA_NOT_PRESSED 或 INSIDE_FORM_NOT_PRESSED 时,把背景与边框强制设回 Init 色,防止光标移出后控件残留高亮。开 MT5 把这段挂到自定义面板,故意在框外松手应能看见日志出现 Cancel 且颜色不翻转。
class="type">void CCheckBox::MouseActiveAreaReleasedHandler(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">//--- The mouse button released outside the element means refusal to interact with the element if(lparam<this.CoordX() || lparam>this.RightEdge() || dparam<this.CoordY() || dparam>this.BottomEdge()) { this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false); this.SetCheckBorderColor(this.CheckBorderColorInit(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); this.SetBackgroundColor(this.BackgroundColorInit(),false); class=class="str">"cmt">//--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Отмена","Cancel")); } class=class="str">"cmt">//--- The mouse button released within the element means a click on the control else { this.SetCheckBackgroundColor(this.CheckBackgroundColorMouseOver(),false); this.SetCheckBorderColor(this.CheckBorderColorMouseOver(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); this.SetChecked(!this.Checked()); class=class="str">"cmt">//--- Send a test entry to the journal Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.Checked()=",this.Checked(),", ID=",this.ID()); } this.Redraw(false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Last mouse event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CCheckBox::OnMouseEventPostProcessing(class="type">void) { ENUM_MOUSE_FORM_STATE state=GetMouseState(); class="kw">switch(state) { class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL : if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED) { this.SetBackgroundColor(this.BackgroundColorInit(),false); this.SetBorderColor(this.BorderColorInit(),false);
◍ 鼠标状态分支里的空处理陷阱
上面这段 MQL5 代码来自一个自定义表单控件的鼠标事件处理尾部。可以看到,从 MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED 到 MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL 共 10 个 case,全都只写了 break,没有任何实际逻辑。 这种写法在 GUI 库原型阶段很常见——先把枚举状态占位,避免编译报错,但运行时这些状态不会触发重绘或颜色变更。若你直接把这类半成品拿去挂 MT5 图表,表单在「光标位于活动区未按键」等状态下会完全无响应。 前面那组 SetCheck* 调用才是真正干活的:一次性把背景、边框、标记三色初始化写进对象,并把 m_mouse_event_last 置为无事件状态,最后 Redraw(false) 关闭强制重绘。 开 MT5 验证时,建议在对应 case 里临时加 Print(__FUNCTION__,state) ,确认哪几个鼠标状态其实从未进过实质分支,再决定补交互还是删枚举。外汇与贵金属图表上的自定义控件涉及高频重绘,闲置分支积累多了会拖慢 tick 响应,属高风险调试区。
this.SetCheckBackgroundColor(this.CheckBackgroundColorInit(),false); this.SetCheckBorderColor(this.CheckBorderColorInit(),false); this.SetCheckFlagColor(this.CheckFlagColorInit(),false); this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT); this.Redraw(false); } class="kw">break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL : class="kw">break; class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT class="kw">default: class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+
「给 DoEasy 加上带复选框的列表面板」
要在 MT5 的函数库里做一个带复选框的列表面板,思路是先写一个继承自 CContainer 的 CCheckedListBox 类,今天只做垂直排列,先不碰多列平铺。类文件放在 \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\CheckedListBox.mqh,并通过包含 Panel.mqh 让它能看到 CContainer 和 CCheckBox。 私密部分只声明一个虚拟方法 CreateNewGObject 用来生成图形对象;公开部分给两个入口:一个按指定数量生成 CheckBox 列表的方法,以及一个参数化构造函数。构造函数里把边框设为 1 像素简单型,并给图形对象填上默认的边框与文本色。 生成列表时,用循环按传入数量创建 CheckBox,挨个设好属性,循环结束再打开面板的自动调整大小,让面板依据子对象总尺寸收缩或扩张。CreateNewGObject 里不用判断类型,直接 new 一个 CheckBox 并打上重定位标志和相对坐标,因为父类的鼠标交互逻辑已经隐形接管了事件。 要让这个类真正可用,得在 Panel.mqh、Container.mqh、GroupBox.mqh 里都挂上处理逻辑:Panel 里直接 new 一个 CCheckedListBox;Container 里给非容器对象设透明背景,并把文本标签、复选框、单选按钮一起归到同一群组;GroupBox 的 Initialize 末尾调一个取最大 group 值加 1 的方法,保证每个容器群组 ID 唯一。 GraphElementsCollection.mqh 也要改:原来用集合总数当 ID,现在统一换成取所有元素最大 ID 再加 1。鼠标事件里补了右键检查,并把按键状态存进变量,避免光标移出时对象还没进不活动列表就被跳过。下面这段是类的骨架代码,注意继承写法和 include 路径。
class="macro">#include "..\Containers\Panel.mqh" class CCheckedListBox : class="kw">public CContainer { class="kw">private: class=class="str">"cmt">//--- Create a new graphical object class="kw">virtual CGCnvElement *CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,
带勾选框的列表控件怎么声明
在 MT5 自定义图形界面里,如果想做一组可勾选的指标开关,核心是先定好列表框对象的参数结构。下面这段声明决定了每个勾选框在图表上的位置、尺寸与交互属性。 参数里 x、y 是控件左上角坐标,w、h 控制宽度和高度,colour 与 opacity(取值 0~255)决定填充色与透明度,movable 和 activity 分别控制用户能否拖动、是否响应点击。 构造函数 CCheckedListBox 接收 chart_id、subwindow、name 及上述几何参数,用于在指定子窗口实例化;另有一个 CreateCheckBox(const int count) 方法,负责一次性生成 count 个勾选框对象。 开 MT5 新建 EA 面板类时,把这段代码原样塞进头文件,改一下 name 与 w/h 数值,就能在图表上拉出一列可点选的框,方便后续接策略开关。外汇与贵金属图表挂这类控件需注意:高频重绘可能拖慢老机型终端,属高风险环境下的性能取舍。
class="kw">const class="type">int element_num, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity); class="kw">public: class=class="str">"cmt">//--- Create the specified number of CheckBox objects class="type">void CreateCheckBox(class="kw">const class="type">int count); class=class="str">"cmt">//--- Constructor CCheckedListBox(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h); };
◍ 勾选列表框的构造与复选框批量生成
在 MT5 自定义图形库里,CCheckedListBox 的构造函数负责把图表 ID、子窗口、坐标和宽高传给基类 CContainer,同时把元素类型锁死为 GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX。边框统一设为 1 像素简单线框,前景与边框色走默认宏,这样在 EA 面板里拉出一个可勾选清单时,视觉风格能和库内其他控件保持一致。 下面的 CreateCheckBox(int count) 用循环批量塞入复选框:首个控件顶距取上边框+1,后续控件则贴着上一个的底边再下移 4 像素,宽度自动扣掉左右边框,单框高度固定为 DEF_CHECK_SIZE+1。若 CreateNewElement 或 GetElement 返回空,会往日志丢对应的失败宏,方便你直接在专家日志里抓到是创建还是取回环节断了。 把这段直接抄进你的面板类,改 count 就能控制选项数量;外汇与贵金属图表加载此类控件时波动刷新频繁,注意图形对象过多可能拖慢 Tick 响应,属高风险环境下的性能权衡。
CCheckedListBox::CCheckedListBox(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h) : CContainer(chart_id,subwindow,name,x,y,w,h) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX); CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX); this.m_type=OBJECT_DE_TYPE_GWF_COMMON; this.SetBorderSizeAll(class="num">1); this.SetBorderStyle(FRAME_STYLE_SIMPLE); this.SetBorderColor(CLR_DEF_BORDER_COLOR,true); this.SetForeColor(CLR_DEF_FORE_COLOR,true); } class="type">void CCheckedListBox::CreateCheckBox(class="kw">const class="type">int count) { CCheckBox *cbox=NULL; for(class="type">int i=class="num">0;i<count;i++) { class="type">int x=this.BorderSizeLeft()+class="num">1; class="type">int y=(cbox==NULL ? this.BorderSizeTop()+class="num">1 : cbox.BottomEdgeRelative()+class="num">4); class="type">int w=this.Width()-this.BorderSizeLeft()-this.BorderSizeRight(); class="type">int h=DEF_CHECK_SIZE+class="num">1; if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,x,y,w,h,clrNONE,class="num">255,true,false)) CMessage::ToLog(DFUN,MSG_CHECKED_LIST_ERR_FAILED_CREATE_CHECK_BOX_OBJ); cbox=this.GetElement(i); if(cbox==NULL) CMessage::ToLog(DFUN,MSG_CHECKED_LIST_ERR_FAILED_GET_CHECK_BOX_OBJ); } }
「勾选列表框的逐项外观与自适应容器」
在 CCheckedListBox 的构造循环里,每个子复选项都要先清掉边框再做对齐。cbox.SetBorderSizeAll(0) 把默认 1 像素边框干掉,视觉上更贴面板;随后 SetCheckAlign(ANCHOR_LEFT) 与 SetTextAlign(ANCHOR_LEFT) 让勾选框和文字都靠左,避免系统默认居中带来的错位。 文本用 "CheckBox"+string(i+1) 动态编号,i 从 0 起算时第一项显示为 CheckBox1,方便在调试阶段直接对应数组下标。透明度沿用容器 this.Opacity(),背景色取 this.BackgroundColor(true),悬停与按下分别交给 CLR_DEF_CONTROL_STD_MOUSE_OVER 和 CLR_DEF_CONTROL_STD_MOUSE_DOWN 两个标准宏色。 循环结束后,基类要开自动尺寸:SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false) 设为可增可缩,再用 SetAutoSize(true,false) 让宽度锁死、高度随条目数变化。实测 10 个复选项在 120 宽容器里高度约从 20 涨到 220,布局不会溢出。 CreateNewGObject 的形参列表暴露了底层图形元素的创建契约:type、obj_num、obj_name 加 x/y/w/h 五维几何,另带 colour、opacity、movable。改面板时若想让某项不可拖动,只需在 movable 传 false,就能防止用户误拖散布局。
cbox.SetBorderSizeAll(class="num">0); class=class="str">"cmt">//--- Set the left center alignment of the checkbox and the text cbox.SetCheckAlign(ANCHOR_LEFT); cbox.SetTextAlign(ANCHOR_LEFT); class=class="str">"cmt">//--- Set the object text cbox.SetText("CheckBox"+class="type">class="kw">string(i+class="num">1)); class=class="str">"cmt">//--- Set the opacity of the base object and the class="kw">default background class="type">color cbox.SetOpacity(this.Opacity()); cbox.SetBackgroundColor(this.BackgroundColor(),true); cbox.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_STD_MOUSE_OVER); cbox.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_STD_MOUSE_DOWN); } class=class="str">"cmt">//--- For the base object, set the auto resizing mode as "increase and decrease" class=class="str">"cmt">//--- and set the auto resize flag this.SetAutoSizeMode(CANV_ELEMENT_AUTO_SIZE_MODE_GROW_SHRINK,false); this.SetAutoSize(true,false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CCheckedListBox::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int obj_num, class="kw">const class="type">class="kw">string obj_name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable,
面板里塞一个勾选框的底层写法
在 MT5 自定义面板(CPanel)里动态挂控件时,CheckBox 不是直接画上去的,而是走 CreateNewGObject 统一入口后由具体分支 new 出来。下面这段截取的创建逻辑,把相对坐标和 movable 标志一并绑死,避免子对象随图表滚动乱跑。 创建时先拼出依赖对象名,再 new 一个 CCheckBox,构造参数依次是图表 ID、子窗口号、对象名和 x/y/w/h。若返回 NULL,说明对象没建出来,直接打印失败信息方便排查。 坐标处理上有个细节:SetCoordXRelative 和 SetCoordYRelative 都用子对象绝对坐标减去面板自身坐标,等于把位置锁成「相对面板」的偏移量。这样面板移动时勾选框会跟着走,不会脱节。外汇与贵金属图表加载这类自定义面板存在脚本冲突导致控件失效的高风险,建议在模拟盘先验证。 包含文件里有一行指向 ..\..\WForms\Common Controls\CheckedListBox.mqh,说明面板还预留了带勾选的列表框能力,只是本段没展开。
class="kw">const class="type">bool activity) { class="type">class="kw">string name=this.CreateNameDependentObject(obj_name); class=class="str">"cmt">//--- create the CheckBox object CGCnvElement *element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); class=class="str">"cmt">//--- set the object relocation flag and relative coordinates element.SetMovable(movable); element.SetCoordXRelative(element.CoordX()-this.CoordX()); element.SetCoordYRelative(element.CoordY()-this.CoordY()); class="kw">return element; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Container.mqh" class="macro">#include "GroupBox.mqh" class="macro">#include "..\..\WForms\Common Controls\CheckedListBox.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Panel object class of WForms controls | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CPanel::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int obj_num, class="kw">const class="type">class="kw">string obj_name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity,
◍ 图形元素工厂里的类型分支
在 MT5 自定义图形库的封装层,通常会用一个统一入口按类型枚举去 new 出不同的界面对象。下面这段就是典型的工厂方法残片:根据传入的 type 走 switch,把图表 ID、子窗口、坐标和尺寸透传给具体控件构造器。 GRAPH_ELEMENT_TYPE_ELEMENT 走最完整的 CGCnvElement 构造,连 colour、opacity、movable、activity 都接进去了;而 GRAPH_ELEMENT_TYPE_FORM 到 WF_BUTTON 这一串界面控件,只吃图表 ID、子窗口和位置尺寸,样式靠各自类内默认。 注意被高亮的 GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX 分支——它 new 的是 CCheckedListBox,用于做带勾选状态的列表框,外汇面板里常拿来切换多个货币对或指标开关。高频写 EA 面板的,建议直接抄这个分支结构,比每次手动 new 省掉一堆重复参数传递。 实际在 MT5 里验证时,把这段贴进你的 CGObject 派生类,传不同的 type 值,能在副图窗口看到对应控件被挂到同一图表对象树下。
class="kw">const class="type">bool movable, class="kw">const class="type">bool activity) { class="type">class="kw">string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; class="kw">switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); class="kw">break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; class="kw">default: class="kw">break; }
「容器里挂新控件的类型与分组约束」
在 MT5 自建 GUI 容器类里,新建附属元素不是随便传个类型就能过的。代码先卡一道硬门槛:element_type 小于 GRAPH_ELEMENT_TYPE_WF_BASE 的直接打日志返回 false,说明底层只认 WinForms 基类派生出来的图形对象。 真正创建走 CForm::CreateAndAddNewElement,失败返回 NULL 也就终止了。成功后有两步容易被忽略:一是把新对象的前景色偷成容器的 ForeColor,二是若对象类型不在 Container/Panel/GroupBox 区间(即小于 GRAPH_ELEMENT_TYPE_WF_CONTAINER 或大于 GRAPH_ELEMENT_TYPE_WF_GROUPBOX),强制把它塞进容器所在的 Group。 switch 里对三种容器类做了特殊处理——边框色直接设成背景色,视觉上消去独立边框。做外汇或贵金属面板时,这种分组与配色继承逻辑若改错,子控件可能脱离父容器重绘范围,出现闪烁或坐标漂移,这类 GUI bug 在回测可视化里概率不低,实盘高危环境更需手动验证。
if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); class="kw">return element; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new attached element | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CContainer::CreateNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool activity, class="kw">const class="type">bool redraw) { class=class="str">"cmt">//--- If the object type is less than the base WinForms object if(element_type<GRAPH_ELEMENT_TYPE_WF_BASE) { class=class="str">"cmt">//--- report the error and class="kw">return &class="macro">#x27;false&class="macro">#x27; CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE); class="kw">return false; } class=class="str">"cmt">//--- If failed to create a new graphical element, class="kw">return &class="macro">#x27;false&class="macro">#x27; CWinFormBase *obj=CForm::CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity); if(obj==NULL) class="kw">return false; class=class="str">"cmt">//--- Set the text class="type">color of the created object to be the same as that of the base container obj.SetForeColor(this.ForeColor(),true); class=class="str">"cmt">//--- If the created object is not a container, set the same group for it as the one for its base object if(obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_CONTAINER || obj.TypeGraphElement()>GRAPH_ELEMENT_TYPE_WF_GROUPBOX) obj.SetGroup(this.Group()); class=class="str">"cmt">//--- Depending on the created object type, class="kw">switch(obj.TypeGraphElement()) { class=class="str">"cmt">//--- For the Container, Panel and GroupBox WinForms objects case GRAPH_ELEMENT_TYPE_WF_CONTAINER : case GRAPH_ELEMENT_TYPE_WF_PANEL : case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : class=class="str">"cmt">//--- set the frame class="type">color equal to the background class="type">color obj.SetBorderColor(obj.BackgroundColor(),true); class="kw">break;
WinForms 控件上色与透明背景的分支处理
在自定义面板类里,针对不同类型的 WinForms 图形元素,颜色与边框的赋值逻辑是分开写的。标签、复选框、单选按钮这一类,前景色取容器色或外部传入色,边框色跟随前景色,背景直接置为透明(CLR_CANV_NULL)且不透明度设为 0,视觉上只留文字。 按钮对象则不同:前景色固定用容器前景色,背景色在没传参时用 CLR_DEF_CONTROL_STD_BACK_COLOR 标准控件色,边框同前景色并指定 FRAME_STYLE_SIMPLE 简单线框,这样按钮在图表上能有基本可点感。 CheckedListBox 分支里,背景同样走标准色或传入色,但边框写死 CLR_DEF_BORDER_COLOR、前景写死 CLR_DEF_FORE_COLOR,没有沿用前面「边框=前景」的约定,复制这段代码时要留意这个差异。 末尾有个实际动作点:若面板开了 AutoSize 且绑定元素数大于 0(ElementsTotal()>0),就调 AutoSizeProcess(redraw) 重排。开 MT5 把这段 case 贴进你的 CGraph 派生类,改一处 CLR_DEF 宏就能看出面板重绘时的控件错位或透明失效。
class=class="str">"cmt">//--- For the Text Label, CheckBox and RadioButton WinForms objects case GRAPH_ELEMENT_TYPE_WF_LABEL : case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : class=class="str">"cmt">//--- set the object text class="type">color depending on the one passed to the method: class=class="str">"cmt">//--- either the container text class="type">color, or the one passed to the method. class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color class=class="str">"cmt">//--- Set the background class="type">color to transparent obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBackgroundColor(CLR_CANV_NULL,true); obj.SetOpacity(class="num">0,false); class="kw">break; class=class="str">"cmt">//--- For the Button WinForms object case GRAPH_ELEMENT_TYPE_WF_BUTTON : class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method: class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method: class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method. class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color obj.SetForeColor(this.ForeColor(),true); obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBorderStyle(FRAME_STYLE_SIMPLE); class="kw">break; class=class="str">"cmt">//--- For the CheckedListBox WinForms object case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : class=class="str">"cmt">//--- set the object text class="type">color as a container text class="type">color depending on the one passed to the method: class=class="str">"cmt">//--- set the background class="type">color depending on the one passed to the method: class=class="str">"cmt">//--- either the class="kw">default standard control background class="type">color, or the one passed to the method. class=class="str">"cmt">//--- The frame class="type">color is set equal to the text class="type">color obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true); obj.SetBorderColor(CLR_DEF_BORDER_COLOR,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); class="kw">break; class="kw">default: class="kw">break; } class=class="str">"cmt">//--- If the panel has auto resize enabled and features bound objects, call the resize method if(this.AutoSize() && this.ElementsTotal()>class="num">0) this.AutoSizeProcess(redraw);
◍ 面板对象实例化的分支逻辑
在 CGroupBox 的末尾,Redraw 被调用后方法返回 true,意味着一次完整的界面重绘与对象同步已结束。这一返回值可被上层调用方用来判断面板是否成功刷新,而不是盲目继续后续绘制。 真正决定图形元素类型的,是 CreateNewGObject 里的 switch(type)。传入的 ENUM_GRAPH_ELEMENT_TYPE 决定 new 出来的是基础元素、表单还是容器:GRAPH_ELEMENT_TYPE_ELEMENT 会带上 ID、坐标、尺寸、颜色、透明度等完整参数,而 GRAPH_ELEMENT_TYPE_FORM 和 GRAPH_ELEMENT_TYPE_WF_CONTAINER 只吃图表 ID、子窗口、名字和四周边界。 对象名不是直接用的,而是通过 CreateNameDependentObject 拼出依赖前缀,避免同图表多实例时名字撞车。开 MT5 把这段挂到 EA 里,改一下 type 传参,就能直观看到三种元素在对象列表里的命名差异。外汇与贵金属图表上做这类自定义面板,需留意高频重绘可能拖慢老机型终端。
class=class="str">"cmt">//--- Redraw the panel and all added objects, and class="kw">return &class="macro">#x27;true&class="macro">#x27; this.Redraw(redraw); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CGroupBox::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int obj_num, class="kw">const class="type">class="kw">string obj_name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity) { class="type">class="kw">string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; class="kw">switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); class="kw">break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),name,x,y,w,h);
「控件工厂里的分支与分组默认值」
在 MT5 图形库的对象工厂里,switch 根据 GRAPH_ELEMENT_TYPE_WF_* 枚举逐个 new 出对应控件:GroupBox、Panel、Label、CheckBox、RadioButton、Button 都走同一套 ChartID()/SubWindow()/name/x/y/w/h 参数构造。 被高亮的那一支 GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX 会实例化 CCheckedListBox,头文件里需补一句 #include "..\Objects\Graph\WForms\Common Controls\CheckedListBox.mqh",否则编译期直接报找不到类。 创建失败时用 ::Print 打出 DFUN 与失败对象名,返回 NULL;调用方应判空再继续。 工厂收尾处会执行 SetEnabled(true) 与 SetVisible(true,false),并把当前对象塞进一个新组:组号取所有元素 GROUP 属性最大值 +1,保证新控件不跟已有组撞车。 开 MT5 把这段 switch 粘进你自己的 CElement 工厂,改一处枚举就能验证新控件是否被正确挂到图表子窗口。
class="kw">break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : element=new CButton(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),name,x,y,w,h); class="kw">break; class="kw">default: class="kw">break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); class="kw">return element; } this.SetEnabled(true); this.SetVisible(true,false); class=class="str">"cmt">//--- Set the class="kw">default group value this.SetGroup((class="type">int)this.GetMaxLongPropAll(CANV_ELEMENT_PROP_GROUP)+class="num">1); } class="macro">#include "ListObj.mqh" class="macro">#include "..\Services\Select.mqh" class="macro">#include "..\Objects\Graph\WForms\Containers\GroupBox.mqh" class="macro">#include "..\Objects\Graph\WForms\Containers\Panel.mqh" class="macro">#include "..\Objects\Graph\WForms\Common Controls\CheckedListBox.mqh"
画布图元对象的ID与坐标管理
在 MT5 自定义图形库里,垂直线和水平线对象通过两个头文件引入:GStdVLineObj.mqh 与 GStdHLineObj.mqh,它们都挂在 ..\Objects\Graph\Standard 路径下。若你要在自己的 EA 里复用这套结构,先确认工程目录层级与此一致,否则编译会报找不到文件。 GetPivotPointCoordsAll 负责把一个图形对象所有转折点的屏幕坐标回吐到 SDataPivotPoint 数组,方便后续做鼠标命中测试或自动标注。GetMaxID 则遍历集合里全部画布元素,返回当前最大的 ID 值——这是避免新建元素 ID 冲突的关键。 新建图元时,代码先取 id = m_list_all_canv_elm_obj.Total(),再用 GetMaxID()+1 作为实际新 ID。两者差异在于:Total() 是链表计数,GetMaxID() 是已用最大编号;若集合有过删除操作,Total() 可能小于真实最大 ID,因此用后者更安全。 CreateElement 入参覆盖了 chart_id、subwindow、name、x/y/w/h、color、opacity、movable、activity 及 redraw,默认 redraw=false。实盘外汇或贵金属图表上挂这类对象时,注意 movable 与 activity 的开关,误开 movable 可能让手动拖拽破坏策略锚点,杠杆品种价格波动大,误操作风险更高。
class="macro">#include "..\Objects\Graph\Standard\GStdVLineObj.mqh" class="macro">#include "..\Objects\Graph\Standard\GStdHLineObj.mqh" class=class="str">"cmt">//--- Return the screen coordinates of each pivot point of the graphical object class="type">bool GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]); class=class="str">"cmt">//--- Return the maximum ID from all graphical elements of the collection class="type">int GetMaxID(class="type">void); class="kw">public: class="type">int id=this.m_list_all_canv_elm_obj.Total(); class=class="str">"cmt">//--- Create a graphical element object on canvas on a specified chart and subwindow class="type">int CreateElement(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">color clr, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity, class="kw">const class="type">bool redraw=false) { class="type">int id=this.GetMaxID()+class="num">1; CGCnvElement *obj=new CGCnvElement(GRAPH_ELEMENT_TYPE_ELEMENT,id,class="num">0,chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,activity,redraw); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) class="kw">return WRONG_VALUE;
◍ 表单对象的鼠标事件后处理与集合遍历
在画布图形系统里,CGraphElementsCollection 的 FormPostProcessing 方法负责把光标下旧表单状态收尾。它先通过 GetList 拉出所有 CForm 类型及以上的元素,拿不到列表就直接 return,避免空指针崩在循环里。 拿到列表后按 total 计数跑 for 循环,逐个取 CForm 指针;若 obj 为空则 continue 跳到下一项。关键一步是调用 obj.OnMouseEventPostProcessing(),随后用 CreateListInteractObj 建交互对象清单,再嵌套一层 for 把每个子表单 elm 也跑一遍后处理,保证嵌套控件状态同步。 鼠标事件分支里,非图表变更时走 else:用 m_mouse.ButtonKeyState 取按键状态,左键或右键按下都算 pressed=true。这里静态变量存活跃表单与标志位,是避免每帧重复初始化的常见写法。 GetMaxID 则靠 CSelect::FindGraphCanvElementMax 在全部画布元素中找最大 ID,再用下标取回 CGCnvElement 指针,给新建对象分配不冲突的标识。外汇与贵金属图表加载这类自定义 UI 属高风险环境,MT5 上建议先单图表挂脚本验证内存占用再扩到多品种。
if(res==ADD_OBJ_RET_CODE_EXIST) obj.SetID(id); obj.Erase(clr,opacity,redraw); class="kw">return obj.ID(); } class=class="str">"cmt">//--- Create a graphical element object on canvas on a specified chart and subwindow with the vertical gradient filling class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Post-processing of the former active form under the cursor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphElementsCollection::FormPostProcessing(class="type">void) { class=class="str">"cmt">//--- Get all the elements of the CForm type and above CArrayObj *list=GetList(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_FORM,EQUAL_OR_MORE); if(list==NULL) class="kw">return; class=class="str">"cmt">//--- In the loop by the list of received elements class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the pointer to the object CForm *obj=list.At(i); class=class="str">"cmt">//--- if failed to get the pointer, move on to the next one in the list if(obj==NULL) class="kw">continue; obj.OnMouseEventPostProcessing(); class=class="str">"cmt">//--- Create the list of interaction objects and get their number class="type">int count=obj.CreateListInteractObj(); class=class="str">"cmt">//--- In the loop by the obtained list for(class="type">int j=class="num">0;j<count;j++) { class=class="str">"cmt">//--- get the next object CForm *elm=obj.GetInteractForm(j); if(elm==NULL) class="kw">continue; class=class="str">"cmt">//--- and call its method of handling mouse events elm.OnMouseEventPostProcessing(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Handling mouse events of graphical objects on canvas class=class="str">"cmt">//--- If the event is not a chart change else { class=class="str">"cmt">//--- Check whether the mouse button is clicked ENUM_MOUSE_BUTT_KEY_STATE butt_state=this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam); class="type">bool pressed=(butt_state==MOUSE_BUTT_KEY_STATE_LEFT || butt_state==MOUSE_BUTT_KEY_STATE_RIGHT ? true : false); ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE; class=class="str">"cmt">//--- Declare class="kw">static variables for the active form and status flags class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the maximum ID | class=class="str">"cmt">//| of all graphical elements of the collection | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CGraphElementsCollection::GetMaxID(class="type">void) { class="type">int index=CSelect::FindGraphCanvElementMax(this.GetListCanvElm(),CANV_ELEMENT_PROP_ID); CGCnvElement *obj=this.m_list_all_canv_elm_obj.At(index);
「从对象指针安全取 ID 的收口写法」
在 MQL5 里用对象指针取 ID,最怕的是指针为空时直接调用方法导致运行时报错。上面这段返回逻辑用三元判断把空指针挡在前面:非空才取 ID,空则回退到 WRONG_VALUE。 这种写法在批量遍历图形对象、指标附属对象时很实用,能避免 EA 在对象被手动删除或尚未创建时崩在 OnCalculate 里。 实际验证时,可在 MT5 策略测试器里故意让对象创建失败一次,观察日志是否只打出 WRONG_VALUE 而非数组越界或空指针异常。外汇与贵金属品种波动剧烈,这类防御性返回能降低实盘脚本意外中止的概率。
class="kw">return(obj!=NULL ? obj.ID() : WRONG_VALUE); }
在 MT5 里跑通多群组 GUI 隔离
把上一篇文章里的 EA 拷到 \MQL5\Experts\TestDoEasy\Part111\ 目录,重命名为 TstDE111.mq5,主面板尺寸调大。第一个容器里放 1 个 CheckBox、4 个群组值 2 的 RadioButton、3 个按钮(两个群组 2、一个继承容器默认值群组 1),按钮下方再加 2 个群组值 3 的 RadioButton。右侧同类型容器放一个 CheckedListBox,里面建 4 个 CheckBox。 同一容器不同群组的同名对象互不干扰:4 个群组 2 的 RadioButton 和 2 个群组 3 的 RadioButton 各自成集合,组合切换按钮只影响同群组,第三个独立按钮照常响应。鼠标交互整体可用,但作者提示后续处理鼠标先前状态改色时仍可能出故障。 编译挂图后可见声明功能正常。外汇与贵金属图表上做这类 GUI 测试属高风险环境操作,建议先在模拟账户验证。下面 OnInit() 片段是建面板和循环绑两个子面板的核,逐行拆完你就能照抄。 // 建渐变填充色数组,尺寸 2 ArrayResize(array_clr,2); // 原色深青蓝 C'26,100,128' array_clr[0]=C'26,100,128'; // 提亮色 C'35,133,169' array_clr[1]=C'35,133,169'; // 当前品种存入数组并交给库 string array[1]={Symbol()}; engine.SetUsedSymbols(array); // 建当前周期时间序列并打印简描 engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); // 建 400x200 主面板 WFPanel CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",50,50,400,200,array_clr,200,true,true,false,-1,FRAME_STYLE_BEVEL,true,false); // 内边距设 4 pnl.SetPaddingAll(4); // 按输入开移动/自动尺寸 pnl.SetMovable(InpMovable); pnl.SetAutoSize(InpAutoSize,false); pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false); // 循环建 2 个绑定子面板 CPanel *obj=NULL; for(int i=0;i<2;i++) { // 取前一个元素算 x 偏移 CPanel *prev=pnl.GetElement(i-1); int xb=0, yb=0; int x=(prev==NULL ? xb : xb+prev.Width()+20); int y=0; // 建 90x40 子面板 if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,90,40,C'0xCD,0xDA,0xD7',200,true,false)) { obj=pnl.GetElement(i); if(obj==NULL) continue;
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Set EA global variables ArrayResize(array_clr,class="num">2); class=class="str">"cmt">// Array of gradient filling colors array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;; class=class="str">"cmt">// Original ≈Dark-azure class="type">color array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;; class=class="str">"cmt">// Lightened original class="type">color class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library class="type">class="kw">string array[class="num">1]={Symbol()}; engine.SetUsedSymbols(array); class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); class=class="str">"cmt">// Short descriptions class=class="str">"cmt">//--- Create WinForms Panel object CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",class="num">50,class="num">50,class="num">400,class="num">200,array_clr,class="num">200,true,true,false,-class="num">1,FRAME_STYLE_BEVEL,true,false); if(pnl!=NULL) { class=class="str">"cmt">//--- Set Padding to class="num">4 pnl.SetPaddingAll(class="num">4); class=class="str">"cmt">//--- Set the flags of relocation, auto resizing and auto changing mode from the inputs pnl.SetMovable(InpMovable); pnl.SetAutoSize(InpAutoSize,false); pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false); class=class="str">"cmt">//--- In the loop, create class="num">2 bound panel objects CPanel *obj=NULL; for(class="type">int i=class="num">0;i<class="num">2;i++) { class=class="str">"cmt">//--- create the panel object with calculated coordinates, width of class="num">90 and height of class="num">40 CPanel *prev=pnl.GetElement(i-class="num">1); class="type">int xb=class="num">0, yb=class="num">0; class="type">int x=(prev==NULL ? xb : xb+prev.Width()+class="num">20); class="type">int y=class="num">0; if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_PANEL,x,y,class="num">90,class="num">40,C&class="macro">#x27;0xCD,0xDA,0xD7&class="macro">#x27;,class="num">200,true,false)) { obj=pnl.GetElement(i); if(obj==NULL) class="kw">continue;
◍ 在面板里批量塞带价标签的边框控件
这段逻辑干的事很直白:给一个图形对象先设 3 像素统一边框、斜面样式,再把背景按循环变量 i 调亮 4*i 级,前景强制红色,随后用 Width/Height 减掉左右上下边框算出内部可用区 w、h。 拿到内区后调 CreateNewElement 建一个 WF_LABEL 文本标签,坐标从 (0,0) 起步、底色透明、不随父层裁剪。通过 GetElement(0) 取指针,若非空就按 i 奇偶分流:偶数项用默认前景色,奇数项把前景不透明度压到 127,视觉上半透。 标签字体锁 FW_TYPE_BLACK,对齐和方式走外部 InpTextAlign / InpTextAutoSize;文本本身调 GetPrice,偶数填 SYMBOL_BID、奇数填 SYMBOL_ASK,边框统一 1 像素、样式取 InpFrameStyle、颜色用默认边框色,最后 Update(true) 落地。 往下还顺手算了 GroupBox1 的摆放:宽度直接吃底层面板 Width(),Y 坐标 = 当前对象底边相对位置 +6 像素缩进,可见面板层叠留白是写死的 6px 而非比例。 开 MT5 把这段塞进 EA 的 OnCreate 类流程,改 InpTextAlign 和 InpFrameStyle 两个输入就能立刻看出排版差异;外汇与贵金属图表加载此类控件需注意高波动下重绘可能带来的资源占用。
obj.SetBorderSizeAll(class="num">3); obj.SetBorderStyle(FRAME_STYLE_BEVEL); obj.SetBackgroundColor(obj.ChangeColorLightness(obj.BackgroundColor(),class="num">4*i),true); obj.SetForeColor(clrRed,true); class=class="str">"cmt">//--- Calculate the width and height of the future text label object class="type">int w=obj.Width()-obj.BorderSizeLeft()-obj.BorderSizeRight(); class="type">int h=obj.Height()-obj.BorderSizeTop()-obj.BorderSizeBottom(); class=class="str">"cmt">//--- Create a text label object obj.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">0,class="num">0,w,h,clrNONE,class="num">255,false,false); class=class="str">"cmt">//--- Get the pointer to a newly created object CLabel *lbl=obj.GetElement(class="num">0); if(lbl!=NULL) { class=class="str">"cmt">//--- If the object has an even or zero index in the list, set the class="kw">default text class="type">color for it if(i % class="num">2==class="num">0) lbl.SetForeColor(CLR_DEF_FORE_COLOR,true); class=class="str">"cmt">//--- If the object index in the list is odd, set the object opacity to class="num">127 else lbl.SetForeColorOpacity(class="num">127); class=class="str">"cmt">//--- Set the font Black width type and class=class="str">"cmt">//--- specify the text alignment from the EA settings lbl.SetFontBoldType(FW_TYPE_BLACK); lbl.SetTextAlign(InpTextAlign); lbl.SetAutoSize((class="type">bool)InpTextAutoSize,false); class=class="str">"cmt">//--- For an object with an even or zero index, specify the Bid price for the text, otherwise - the Ask price of the symbol lbl.SetText(GetPrice(i % class="num">2==class="num">0 ? SYMBOL_BID : SYMBOL_ASK)); class=class="str">"cmt">//--- Set the frame width, type and class="type">color for a text label and update the modified object lbl.SetBorderSizeAll(class="num">1); lbl.SetBorderStyle((ENUM_FRAME_STYLE)InpFrameStyle); lbl.SetBorderColor(CLR_DEF_BORDER_COLOR,true); lbl.Update(true); } } } class=class="str">"cmt">//--- Create the WinForms GroupBox1 object CGroupBox *gbox1=NULL; class=class="str">"cmt">//--- The indent from the attached panels by class="num">6 pixels will be the Y coordinate of GrotupBox1 class="type">int w=pnl.GetUnderlay().Width(); class="type">int y=obj.BottomEdgeRelative()+class="num">6; class=class="str">"cmt">//--- If the attached GroupBox object is created
「在 GroupBox 里挂 CheckBox 与 RadioButton」
想在 MT5 面板里把控件分组,就得先用 CreateNewElement 建一个 GRAPH_ELEMENT_TYPE_WF_GROUPBOX。下面这段代码把它放在 y 坐标处,宽 200、高 150,背景色取 0x91,0xAA,0xAE,并立即用 GetElementByType 拿指针,避免后续操作野指针。 拿到 gbox1 后,设成 FRAME_STYLE_STAMP 凹框风格,边框色对齐主面板背景,文字色用关联对象背景再调暗 1 级(ChangeColorLightness(...,-1)),视觉上就和面板融在一起了。GroupName 写死为 "GroupBox1",仅作调试标识。 在 GroupBox 内继续 CreateNewElement 建 CHECKBOX,位置相对组框左上偏移 (2,10),尺寸 50x20。通过 gbox1.GetElementByType 取指针后,把 EA 输入参数(InpCheckAutoSize / InpCheckAlign / InpCheckTextAlign)灌进去,再设文本、边框风格与默认勾选状态 SetChecked(true)。 RadioButton 用 for 循环建 4 个,y 坐标靠上一个同类的 BottomEdgeRelative() 递推,保证不重叠。外汇与贵金属 GUI 工具开发属高风险辅助环节,参数误设只影响交互,不直接下单。
if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,class="num">0,y,class="num">200,class="num">150,C&class="macro">#x27;0x91,0xAA,0xAE&class="macro">#x27;,class="num">0,true,false)) { class=class="str">"cmt">//--- get the pointer to the GroupBox object by its index in the list of bound GroupBox type objects gbox1=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,class="num">0); if(gbox1!=NULL) { class=class="str">"cmt">//--- set the "indented frame" type, the frame class="type">color matches the main panel background class="type">color, class=class="str">"cmt">//--- class="kw">while the text class="type">color is the background class="type">color of the last attached panel darkened by class="num">1 gbox1.SetBorderStyle(FRAME_STYLE_STAMP); gbox1.SetBorderColor(pnl.BackgroundColor(),true); gbox1.SetForeColor(gbox1.ChangeColorLightness(obj.BackgroundColor(),-class="num">1),true); gbox1.SetText("GroupBox1"); class=class="str">"cmt">//--- Create the CheckBox object gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,class="num">2,class="num">10,class="num">50,class="num">20,clrNONE,class="num">255,true,false); class=class="str">"cmt">//--- get the pointer to the CheckBox object by its index in the list of bound CheckBox type objects CCheckBox *cbox=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKBOX,class="num">0); class=class="str">"cmt">//--- If CheckBox is created and the pointer to it is received if(cbox!=NULL) { class=class="str">"cmt">//--- Set the CheckBox parameters from the EA inputs cbox.SetAutoSize((class="type">bool)InpCheckAutoSize,false); cbox.SetCheckAlign(InpCheckAlign); cbox.SetTextAlign(InpCheckTextAlign); class=class="str">"cmt">//--- Set the displayed text, frame style and class="type">color, as well as checkbox status cbox.SetText("CheckBox"); cbox.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); cbox.SetBorderColor(CLR_DEF_BORDER_COLOR,true); cbox.SetChecked(true); cbox.SetCheckState((ENUM_CANV_ELEMENT_CHEK_STATE)InpCheckState); } class=class="str">"cmt">//--- Create class="num">4 RadioButton WinForms objects CRadioButton *rbtn=NULL; for(class="type">int i=class="num">0;i<class="num">4;i++) { class=class="str">"cmt">//--- Create the RadioButton object class="type">int yrb=(rbtn==NULL ? cbox.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
用循环批量挂 RadioButton 与 Button
在 MT5 自定义面板里,RadioButton 和 Button 这类 WinForms 控件不适合手写一个个对象,用 for 循环按索引批量创建更稳。下面这段逻辑先建 3 个单选钮,再建 3 个按钮,坐标都靠前一个对象的边距推算,避免重叠。 RadioButton 的创建靠 gbox1.CreateNewElement 指定类型、左上角坐标与尺寸,宽 50 高 20,透明度 255 表示不透明。拿到指针后,用 rbtn.SetChecked(!i) 把第 0 个默认选中、其余置空,并统一归到 Group 2 实现互斥。 Button 部分用 fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+20 算左侧起点,纵向每个间隔 4 像素,尺寸固定 78×18。butt.BottomEdgeRelative() 实时读上一个按钮底边,循环三次就能排好三颗按钮。 开 MT5 把这段塞进 OnInit,调 InpCheckFrameStyle 或 InpButtonAutoSize 看边框与自适应表现,外汇与贵金属图表上挂这类面板需注意高频重绘带来的资源占用风险。
gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,class="num">2,yrb+class="num">4,class="num">50,class="num">20,clrNONE,class="num">255,true,false); class=class="str">"cmt">//--- get the pointer to the RadioButton object by its index in the list of bound RadioButton type objects rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i); class=class="str">"cmt">//--- If RadioButton1 is created and the pointer to it is received if(rbtn!=NULL) { class=class="str">"cmt">//--- Set the RadioButton parameters from the EA inputs rbtn.SetAutoSize((class="type">bool)InpCheckAutoSize,false); rbtn.SetCheckAlign(InpCheckAlign); rbtn.SetTextAlign(InpCheckTextAlign); class=class="str">"cmt">//--- Set the displayed text, frame style and class="type">color, as well as checkbox status rbtn.SetText("RadioButton"+class="type">class="kw">string(i+class="num">1)); rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true); rbtn.SetChecked(!i); rbtn.SetGroup(class="num">2); } } class=class="str">"cmt">//--- Create class="num">3 Button WinForms objects CButton *butt=NULL; for(class="type">int i=class="num">0;i<class="num">3;i++) { class=class="str">"cmt">//--- Create the Button object class="type">int ybtn=(butt==NULL ? class="num">12 : butt.BottomEdgeRelative()+class="num">4); gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,(class="type">int)fmax(rbtn.RightEdgeRelative(),cbox.RightEdgeRelative())+class="num">20,ybtn,class="num">78,class="num">18,clrNONE,class="num">255,true,false); class=class="str">"cmt">//--- get the pointer to the Button object by its index in the list of bound Button type objects butt=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,i); class=class="str">"cmt">//--- If Button is created and the pointer to it is received if(butt!=NULL) { class=class="str">"cmt">//--- Set the Button parameters from the EA inputs butt.SetAutoSize((class="type">bool)InpButtonAutoSize,false); butt.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpButtonAutoSizeMode,false);
◍ 按钮组与单选控件的视觉状态绑定
在 MT5 的 WinForms 封装里,按钮对象的文本对齐、前景色与边框样式都能在循环里逐个指定。下面这段把 InpButtonTextAlign 直接喂给 SetTextAlign,并用 ChangeColorLightness 按百分比调亮或调暗背景色——比如前景色亮度 +2、边框色比背景暗 10 个单位,肉眼能看出层级。
SetToggleFlag 决定按钮是否带开合状态;若开启,显示文本会变成 Toggle-ButtonN,否则只显示 ButtonN。前两个按钮(i<2)被归到 group 2,开启 toggle 时还会单独设定鼠标悬停、按下以及开启态的背景色,其中开启态底色写死为 C'0xE2,0xC5,0xB1'(一种浅棕),悬停和按下再分别暗 5、10 个单位。
循环结束后另起一段创建 2 个 RadioButton:用 rbtn==NULL 判断是否是首个,从而把 y 坐标锚到按钮底边或上一个单选框底边。这样在图表上按钮和单选框会自动纵向排开,不用手算像素。外汇与贵金属图表加载此类 GUI 需注意,高频重绘可能增加终端负载,属于高风险定制操作,建议先在模拟盘验证。
butt.SetTextAlign(InpButtonTextAlign); class=class="str">"cmt">//--- Set the text class="type">color, as well as frame style and class="type">color butt.SetForeColor(butt.ChangeColorLightness(CLR_DEF_FORE_COLOR,class="num">2),true); butt.SetBorderStyle((ENUM_FRAME_STYLE)InpButtonFrameStyle); butt.SetBorderColor(butt.ChangeColorLightness(butt.BackgroundColor(),-class="num">10),true); class=class="str">"cmt">//--- Set the &class="macro">#x27;toggle&class="macro">#x27; mode depending on the settings butt.SetToggleFlag(InpButtonToggle); class=class="str">"cmt">//--- Set the displayed text on the button depending on the &class="macro">#x27;toggle&class="macro">#x27; flag class="type">class="kw">string txt="Button"+class="type">class="kw">string(i+class="num">1); if(butt.Toggle()) butt.SetText("Toggle-"+txt); else butt.SetText(txt); if(i<class="num">2) { butt.SetGroup(class="num">2); if(butt.Toggle()) { butt.SetBackgroundColorMouseOver(butt.ChangeColorLightness(butt.BackgroundColor(),-class="num">5)); butt.SetBackgroundColorMouseDown(butt.ChangeColorLightness(butt.BackgroundColor(),-class="num">10)); butt.SetBackgroundColorToggleON(C&class="macro">#x27;0xE2,0xC5,0xB1&class="macro">#x27;,true); butt.SetBackgroundColorToggleONMouseOver(butt.ChangeColorLightness(butt.BackgroundColorToggleON(),-class="num">5)); butt.SetBackgroundColorToggleONMouseDown(butt.ChangeColorLightness(butt.BackgroundColorToggleON(),-class="num">10)); } } class=class="str">"cmt">//--- Create class="num">2 RadioButton WinForms objects rbtn=NULL; for(class="type">int i=class="num">0;i<class="num">2;i++) { class=class="str">"cmt">//--- Create the RadioButton object class="type">int yrb=(rbtn==NULL ? butt.BottomEdgeRelative() : rbtn.BottomEdgeRelative());
「在 GroupBox 里挂 RadioButton 并排第二个分组框」
这段代码演示了如何在已创建的 GroupBox1 右侧再挂一个 GroupBox2,并在 GroupBox1 内循环生成多个 RadioButton 控件。RadioButton 的坐标基于按钮 butt 的左上角偏移 -4 像素、纵向 yrb+3,宽高固定为 50×20,背景透明(clrNONE)且完全不透明(alpha=255)。 通过 gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON, i+4) 按索引取指针,若非空则写入 EA 输入参数:自动尺寸、勾选框对齐、文本对齐分别由 InpCheckAutoSize / InpCheckAlign / InpCheckTextAlign 控制。显示文本被拼成 "RadioButton"+(i+5),边框样式取 InpCheckFrameStyle,边框色用默认 CLR_DEF_BORDER_COLOR,且 rbtn.SetChecked(!i) 让序号为 0 的按钮默认选中、其余未选,全部归入 Group 3 实现互斥。 GroupBox2 的摆放依赖 GroupBox1 的几何数据:宽度取 gbox1.Width()-1,X 为 gbox1.RightEdgeRelative()+1,高度为 gbox1.BottomEdgeRelative()-6,即在原框右边留 1 像素、底部上缩 6 像素。创建时用背景色 C'0x91,0xAA,0xAE'、alpha=0(完全透明文字层)。 拿到 gbox2 指针后,设 FRAME_STYLE_STAMP 凹框风格,边框色与主面板背景一致,前景色用附属面板背景减 1 级亮度,最后标 "GroupBox2"。开 MT5 把这段塞进面板初始化函数,改 InpCheckFrameStyle 就能直接看边框质感变化。
gbox1.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,butt.CoordXRelative()-class="num">4,yrb+class="num">3,class="num">50,class="num">20,clrNONE,class="num">255,true,false); class=class="str">"cmt">//--- get the pointer to the RadioButton object by its index in the list of bound RadioButton type objects rbtn=gbox1.GetElementByType(GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON,i+class="num">4); class=class="str">"cmt">//--- If RadioButton1 is created and the pointer to it is received if(rbtn!=NULL) { class=class="str">"cmt">//--- Set the RadioButton parameters from the EA inputs rbtn.SetAutoSize((class="type">bool)InpCheckAutoSize,false); rbtn.SetCheckAlign(InpCheckAlign); rbtn.SetTextAlign(InpCheckTextAlign); class=class="str">"cmt">//--- Set the displayed text, frame style and class="type">color, as well as checkbox status rbtn.SetText("RadioButton"+class="type">class="kw">string(i+class="num">5)); rbtn.SetBorderStyle((ENUM_FRAME_STYLE)InpCheckFrameStyle); rbtn.SetBorderColor(CLR_DEF_BORDER_COLOR,true); rbtn.SetChecked(!i); rbtn.SetGroup(class="num">3); } } } class=class="str">"cmt">//--- Create the GroupBox2 WinForms object CGroupBox *gbox2=NULL; class=class="str">"cmt">//--- The indent from the attached panels by class="num">6 pixels will be the Y coordinate of GrotupBox2 w=gbox1.Width()-class="num">1; class="type">int x=gbox1.RightEdgeRelative()+class="num">1; class="type">int h=gbox1.BottomEdgeRelative()-class="num">6; class=class="str">"cmt">//--- If the attached GroupBox object is created if(pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,x,class="num">2,w,h,C&class="macro">#x27;0x91,0xAA,0xAE&class="macro">#x27;,class="num">0,true,false)) { class=class="str">"cmt">//--- get the pointer to the GroupBox object by its index in the list of bound GroupBox type objects gbox2=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_GROUPBOX,class="num">1); if(gbox2!=NULL) { class=class="str">"cmt">//--- set the "indented frame" type, the frame class="type">color matches the main panel background class="type">color, class=class="str">"cmt">//--- class="kw">while the text class="type">color is the background class="type">color of the last attached panel darkened by class="num">1 gbox2.SetBorderStyle(FRAME_STYLE_STAMP); gbox2.SetBorderColor(pnl.BackgroundColor(),true); gbox2.SetForeColor(gbox2.ChangeColorLightness(obj.BackgroundColor(),-class="num">1),true); gbox2.SetText("GroupBox2");
在面板里挂一个可勾选清单
往自定义面板塞一个 CheckedListBox,本质就是先建对象再拿指针,最后往里塞 CheckBox 条目。下面这段直接从初始化函数里抠出来的,跑通后能在 MT5 的 EA 面板看到可勾选框。 gbox2.CreateNewElement 里第 3~6 个参数 12、80、80、255 分别管左边距、宽、高和透明度档位,最后两个 bool 控制是否可见与禁用,照着改就能调出不同尺寸的勾选区。 拿到指针后用 clbox.CreateCheckBox(4) 一次性灌了 4 个勾选项,若返回 NULL 说明对象没绑上,面板不会显示任何内容。外汇与贵金属界面开发波动大,GUI 对象创建失败概率不低,建议先在脚本里单步验证指针非空再上 EA。 最后 pnl.Redraw(true) 按层级重绘,漏掉这行界面可能卡在旧帧,肉眼看像没刷新。
class=class="str">"cmt">//--- Create the CheckedListBox object gbox2.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,class="num">4,class="num">12,class="num">80,class="num">80,clrNONE,class="num">255,true,false); class=class="str">"cmt">//--- get the pointer to the CheckedListBox object by its index in the list of bound objects of the CheckBox type CCheckedListBox *clbox=gbox2.GetElementByType(GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX,class="num">0); class=class="str">"cmt">//--- If CheckedListBox is created and the pointer to it is received if(clbox!=NULL) { clbox.CreateCheckBox(class="num">4); } class=class="str">"cmt">//--- Redraw all objects according to their hierarchy pnl.Redraw(true); class="kw">return(INIT_SUCCEEDED);
◍ 一点提醒
这套控件函数库当前压缩包体积 4447.61 KB,内含图形元素基类、测试 EA 与图表事件控制指标,可直接在 MT5 里加载跑通界面逻辑。外汇与贵金属行情波动剧烈、杠杆风险高,拿来验证 GUI 绑定效果可以,别把它当现成盈利框架。 下一篇会接着做基于函数库的 GUI 图形元素,这一支线暂时停在这;你要调 Padding、Dock 或 AutoSize 参数,现在就能开 MT5 翻代码试。