DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象·进阶篇
◍ GUI 控件的消息常量与多语映射
在 MT5 自定义面板开发里,图形对象归属与控件错误都以消息 ID 常量形式定义,例如 MSG_GRAPH_OBJ_BELONG_PROGRAM 表示对象归程序所有,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE 则要求被创建对象必须是 WinForms Base 类型或其派生类。 这些常量后面紧跟一组俄文/英文双语字符串映射,像 "Базовый стандартный элемент управления WinForms" 对应 "Basic Standard WinForms Control",共覆盖 Label、CheckBox、RadioButton、Button 及四类 ListBox 派生控件,合计 13 条标准控件名映射。 整数属性段里能看到 ListBox 的多列与列宽控制:MSG_CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN 管横向列显示,MSG_CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH 定每列像素宽;字符串属性则暴露对象名、资源名与元素文本三个可读字段。 开 MT5 把这段常量表塞进枚举或结构,编译后拖一个 CPanel 出来,若报 OBJ_MUST_BE_WFBASE,就能直接对照常量定位是不是基类继承写漏了。
MSG_GRAPH_OBJ_BELONG_PROGRAM, class=class="str">"cmt">// Graphical object belongs to a program MSG_GRAPH_OBJ_BELONG_NO_PROGRAM, class=class="str">"cmt">// Graphical object does not belong to a program class=class="str">"cmt">//--- CPanel MSG_PANEL_OBJECT_ERR_FAILED_CREATE_UNDERLAY_OBJ, class=class="str">"cmt">// Failed to create the underlay object MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE, class=class="str">"cmt">// Error. The created object should be of WinForms Base type or be derived from it class=class="str">"cmt">//--- ElementsListBox MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ, class=class="str">"cmt">// Failed to get a graphical element class=class="str">"cmt">//--- CButtonListBox MSG_BUTT_LIST_ERR_FAILED_SET_GROUP_BUTTON, class=class="str">"cmt">// Failed to set the group for the button with the index MSG_BUTT_LIST_ERR_FAILED_SET_TOGGLE_BUTTON, class=class="str">"cmt">// Failed to set the Toggle flag to the button with the index class=class="str">"cmt">//--- Integer properties of graphical elements MSG_CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_DOWN, class=class="str">"cmt">// Color of control checkbox when clicking on the control MSG_CANV_ELEMENT_PROP_CHECK_FLAG_COLOR_MOUSE_OVER, class=class="str">"cmt">// Color of control checkbox when hovering the mouse over the control MSG_CANV_ELEMENT_PROP_LIST_BOX_MULTI_COLUMN, class=class="str">"cmt">// Horizontal display of columns in the ListBox control MSG_CANV_ELEMENT_PROP_LIST_BOX_COLUMN_WIDTH, class=class="str">"cmt">// Width of each ListBox control column class=class="str">"cmt">//--- Real properties of graphical elements class=class="str">"cmt">//--- String properties of graphical elements MSG_CANV_ELEMENT_PROP_NAME_OBJ, class=class="str">"cmt">// Graphical element object name MSG_CANV_ELEMENT_PROP_NAME_RES, class=class="str">"cmt">// Graphical resource name MSG_CANV_ELEMENT_PROP_TEXT, class=class="str">"cmt">// Graphical element text }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- WinForms standard {"Базовый стандартный элемент управления WinForms","Basic Standard WinForms Control"}, {"Элемент управления \"Label\"","Control element \"Label\""}, {"Элемент управления \"CheckBox\"","Control element \"CheckBox\""}, {"Элемент управления \"RadioButton\"","Control element \"RadioButton\""}, {"Элемент управления \"Button\"","Control element \"Button\""}, {"Базовый объект-список Windows Forms элементов","Basic Windows Forms List Object"}, {"Элемент управления \"ListBox\"","Control element \"ListBox\""}, {"Элемент управления \"CheckedListBox\"","Control element \"CheckedListBox\""}, {"Элемент управления \"ButtonListBox\"","Control element \"ButtonListBox\""}, {"Графический объект принадлежит программе","The graphic object belongs to the program"}, {"Графический объект не принадлежит программе","The graphic object does not belong to the program"}, class=class="str">"cmt">//--- CPanel {"Не удалось создать объект-подложку","Failed to create underlay object"}, {"Ошибка. Создаваемый объект должен иметь тип WinForms Base или быть его наследником","Error. The object being created must be of type WinForms Base or be derived from it"}, class=class="str">"cmt">//--- ElementsListBox
图形元素报错表与类型映射的底层写法
在自定义 GUI 库里,报错字符串常以俄英双语文组形式集中声明,方便后续通过索引调出提示。下面这段初始化数据里能看到 CButtonListBox 相关失败信息,以及 ListBox 水平列显示、列宽等整数属性的描述串,说明控件属性在源码层就已被拆成可枚举的文本常量。 GraphObjectType 这个方法用一行强转把 ENUM_OBJECT_DE_TYPE 折算回标准 ENUM_OBJECT:返回值是 obj_type 减去 OBJECT_DE_TYPE_GSTD_OBJ 再减 1。若你改了枚举基线,这处偏移量不跟着调,类型识别会整体错位。 TypeElementDescription 有两个重载,一个吃 ENUM_GRAPH_ELEMENT_TYPE 参数,一个无参读内部状态,均走 CMessage::Text 取多语文案。GRAPH_ELEMENT_TYPE_STANDARD 与 GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED 的区分,决定了面板元素在运行期被归纳为哪一类基础控件。 开 MT5 建个空 EA 把这段抄进类定义,编译后断点看 GraphObjectType(OBJECT_DE_TYPE_GSTD_OBJ+1) 的返回值,应当落回 OBJECT_LABEL 一类的标准对象枚举,验证偏移逻辑没写反。
ENUM_OBJECT GraphObjectType(const ENUM_OBJECT_DE_TYPE obj_type) const { class="kw">return ENUM_OBJECT(obj_type-OBJECT_DE_TYPE_GSTD_OBJ-class="num">1); } class="type">class="kw">string CGBaseObj::TypeElementDescription(const ENUM_GRAPH_ELEMENT_TYPE type) { class="kw">return ( type==GRAPH_ELEMENT_TYPE_STANDARD ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD) : type==GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED) :
「图元类型到界面文案的映射写法」
在 MT5 自定义指标或 EA 的 GUI 模块里,常需要把内部枚举 GRAPH_ELEMENT_TYPE_* 转成可读字符串用于调试面板或日志。下面这段三元表达式链就是典型做法:按 type 值逐级判断,命中后调用 CMessage::Text 取对应多语言文案。 注意分支覆盖了基础图元(ELEMENT / SHADOW_OBJ / FORM / WINDOW)、WinForms 层(UNDERLAY / BASE / 容器 / 标准控件)三大类,其中标准控件又细分到 LABEL、CHECKBOX、RADIOBUTTON、BUTTON 及三种 LIST_BOX。最后三行被标黄,说明在原工程里属于后续待补或易错分支,开 MT5 搜 GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX 能确认它是否已在你的终端版本头文件中定义。 实际改写时建议把这种长链式三元拆成 switch(type),否则超过 10 个分支后编译器报警且难维护;外汇与贵金属图表上挂此类面板需注意,GUI 对象过多会拖慢 tick 处理,存在滑点扩大的高风险。
type==GRAPH_ELEMENT_TYPE_ELEMENT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_ELEMENT) : type==GRAPH_ELEMENT_TYPE_SHADOW_OBJ ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_SHADOW_OBJ) : type==GRAPH_ELEMENT_TYPE_FORM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_FORM) : type==GRAPH_ELEMENT_TYPE_WINDOW ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WINDOW) : class=class="str">"cmt">//--- WinForms type==GRAPH_ELEMENT_TYPE_WF_UNDERLAY ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_UNDERLAY) : type==GRAPH_ELEMENT_TYPE_WF_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BASE) : class=class="str">"cmt">//--- Containers type==GRAPH_ELEMENT_TYPE_WF_CONTAINER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CONTAINER) : type==GRAPH_ELEMENT_TYPE_WF_GROUPBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_GROUPBOX) : type==GRAPH_ELEMENT_TYPE_WF_PANEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_PANEL) : class=class="str">"cmt">//--- Standard controls type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE) : type==GRAPH_ELEMENT_TYPE_WF_LABEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL) : type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX) : type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON) : type==GRAPH_ELEMENT_TYPE_WF_BUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON) : type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX) :
◍ 按钮松开时的状态回退与切换逻辑
在自定义 GUI 控件的鼠标事件处理里,左键在控件活动区外松开和区内松开走的是两套分支。区外松开代表用户反悔,不触发任何业务回调,只负责把视觉状态复位。 具体看 CButton::MouseActiveAreaReleasedHandler:先用 lparam(X 坐标)和 dparam(Y 坐标)比对 CoordX/RightEdge/CoordY/BottomEdge,只要落在这四个边界之外,就进入取消分支。普通按钮调 SetBackgroundColor(BackgroundColorInit()) 恢复初始底色;Toggle 按钮则按当前 State() 决定恢复初始色还是按下初始色,同时边框色也复位为 BorderColorInit(),并往日志打一条 Cancel 提示。 若在边界内松开,才是真正点击。普通按钮设为 MouseOver 底色;Toggle 按钮且不属于按钮组(GroupButtonFlag() 为 false)时,执行 SetState(!State()) 翻转状态。属于组的 Toggle 按钮此处不自行翻转,状态由组管理器统一协调。 开 MT5 新建一个带 Toggle 按钮的面板,故意在按钮边缘外松左键,能在专家日志看到 Cancel 且颜色回退,验证这套边界判断的有效性。
class="type">void CButton::MouseActiveAreaReleasedHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,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()) { class=class="str">"cmt">//--- If this is a simple button, set the initial background class="type">color if(!this.Toggle()) this.SetBackgroundColor(this.BackgroundColorInit(),false); class=class="str">"cmt">//--- If this is the toggle button, set the initial class="type">color depending on whether the button is pressed or not else this.SetBackgroundColor(!this.State() ? this.BackgroundColorInit() : this.BackgroundColorToggleONInit(),false); class=class="str">"cmt">//--- Set the initial frame class="type">color this.SetBorderColor(this.BorderColorInit(),false); class=class="str">"cmt">//--- Send the test message 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 { class=class="str">"cmt">//--- If this is a simple button, set the class="type">color for "The cursor is over the active area" status if(!this.Toggle()) this.SetBackgroundColor(this.BackgroundColorMouseOver(),false); class=class="str">"cmt">//--- If this is the toggle button, else { class=class="str">"cmt">//--- if the button does not work in the group, set its state to the opposite, if(!this.GroupButtonFlag()) this.SetState(!this.State()); } } }
按钮状态切换与组标记的内部实现
在自定义按钮的事件处理里,若当前未记录按下状态,代码会先通过 SetState(true) 把按钮置为已按,再依据 State() 返回值切换鼠标悬停时的背景色:按下态用 BackgroundColorToggleONMouseOver(),未按下态用 BackgroundColorMouseOver()。这一步直接决定了面板在交互时的视觉反馈是否跟手。 调试阶段可在点击事件里用 Print 输出 this.State()、ID() 与 Group(),日志会打印类似「Click, this.State()=true, ID=1, Group=panel_01」的行,方便确认对象树里的归属。注意外汇与贵金属图表上的 UI 测试同样受平台重绘机制影响,高频 Print 可能拖慢 EA 响应,属可控风险。 State() 与 GroupButtonFlag() 都只是对 CANV_ELEMENT_PROP 系列属性的薄封装:前者读按钮按下的布尔值,后者读组按钮标记。SetGroupButtonFlag() 写入该标记后,同组按钮便可共享排他逻辑,避免手动维护数组。
else if(!this.State()) this.SetState(true); this.SetBackgroundColor(this.State() ? this.BackgroundColorToggleONMouseOver() : this.BackgroundColorMouseOver(),false); } Print(DFUN_ERR_LINE,TextByLanguage("Щелчок","Click"),", this.State()=",this.State(),", ID=",this.ID(),", Group=",this.Group()); this.SetBorderColor(this.BorderColorMouseOver(),false); } this.Redraw(false); } class="type">bool State(class="type">void) const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_STATE); } class="type">void SetGroupButtonFlag(const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP,flag); } class="type">bool GroupButtonFlag(class="type">void) const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_BUTTON_GROUP); } class="type">void SetBackgroundColorToggleON(const class="type">color colour,const class="type">bool set_init_color)
「给列表控件造一个共用底座」
WinForms 里多种列表控件行为高度重叠,与其在每个子类里重写一遍,不如在 \MQL5\Include\DoEasy\Objects\Graph\WForms\Common Controls\ 下新建 ElementsListBox.mqh,用 CElementsListBox 作为基准容器类,让 CheckedListBox 等后代直接继承共用逻辑。 基类里私密方法 GetCoordsObj 负责算下一个对象的落点;受保护方法 CreateElements 按指定类型和数量批量建对象;公开部分留了参数化构造函数,以及控制“是否允许按列排布”“每列宽度”的接口。构造函数里把图形元素类型、库对象类型定死,边框设 1 像素,默认色和禁列标志一并初始化,列宽先填 0。 坐标算法的关键是:以首个对象初始坐标为基准,叠加前一个对象位置推算下一个。若开了多列模式,只比 Y 坐标是否超出面板——没超就堆在前一个下方,超了就回到初始 Y、接在前一个右边框旁,等于起新列。因为不同对象高度可能不同,算出来的底坐标可能戳出面板,所以父类干脆在摆完之后调一次“按内容自适应面板尺寸”,比预先精算未来对象占地更省事。 把 CheckedListBox.mqh 里的面板包含改成继承 CElementsListBox 后,原创建方法只要传“对象宽度”和“新列宽”两个参数进父类,循环里逐个补属性即可。改写后代码行数明显下降,逻辑也更顺。外汇与贵金属行情波动剧烈、杠杆风险高,这类 GUI 改造仅影响交互层,不改变任何下单逻辑与胜率。
class="macro">#include "..\Containers\Container.mqh" class CElementsListBox : class="kw">public CContainer { class="kw">private: class=class="str">"cmt">//--- Return the coordinates of the next object placed in the list class="type">void GetCoordsObj(CWinFormBase *obj,class="type">int &x,class="type">int &y); class="kw">protected: class=class="str">"cmt">//--- Create the specified number of specified WinForms objects class="type">void CreateElements(ENUM_GRAPH_ELEMENT_TYPE element_type, const class="type">int count,