DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象·进阶篇
📘

DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象·进阶篇

第 2/3 篇

◍ 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,就能直接对照常量定位是不是基类继承写漏了。

MQL5 / C++
  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 一类的标准对象枚举,验证偏移逻辑没写反。

MQL5 / C++
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 处理,存在滑点扩大的高风险。

MQL5 / C++
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 且颜色回退,验证这套边界判断的有效性。

MQL5 / C++
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() 写入该标记后,同组按钮便可共享排他逻辑,避免手动维护数组。

MQL5 / C++
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 改造仅影响交互层,不改变任何下单逻辑与胜率。

MQL5 / C++
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,

常见问题

在图元类型到文案的映射表里按语言索引存字符串,绘制前根据当前语言取对应值即可,避免硬编码中文或英文。
检查按钮松开事件里是否调用了状态回退函数;若用了组标记,需确认同组其他按钮的选中态切换在释放时同步重置。
可以,把控件代码或截图发给小布,它能对照常见图形元素报错表指出类型映射遗漏和状态回退缺失,省去手动逐行查。
能,抽一个基准列表对象作底座,把滚动、项容器、选中逻辑放进去,具体控件只扩展交互和渲染,减少重复代码。
通常界面显示空白、文案变成代号或点击无响应,因为渲染层按类型取资源失败,需在初始化时校验映射完整性。