MQL5 MVC架构中表格视图与控制器组件:简单控件·进阶篇
🧩

MQL5 MVC架构中表格视图与控制器组件:简单控件·进阶篇

(2/3)· 基类优化后,文本标签与各类按钮如何成为复合界面元素的积木

进阶 第 2/3 篇
很多人在 MQL5 里写控件喜欢从零画按钮,结果事件处理和重绘逻辑越堆越乱。其实基于 MVC 把视图和控制器拆开,先用简单控件打底,后面复合组件会省掉大量重复劳动。

◍ 基类对象怎么管 ID 和描述

在 MT5 自定义类库里,CBaseObj 承担的是最底层对象容器角色:它把名称、ID 和类型封装到一起,让上层指标或订单管理类能统一调用。构造函数里 m_id 默认赋 -1,意味着未显式 SetID 之前对象处于“匿名”状态,这点开 MT5 调试时很容易在日志里看到 ID -1 的打印。 下面这段是类声明里最关键的存取与虚函数骨架,逐行看一遍就能明白扩展点在哪里。 void SetID(const int id) { this.m_id=id; } //--- Return (1) name and (2) ID string Name(void) const { return ::ShortArrayToString(this.m_name); } int ID(void) const { return this.m_id; } //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(ELEMENT_TYPE_BASE); } //--- (1) Return and (2) display the object description in the journal virtual string Description(void); virtual void Print(void); //--- Constructor/destructor CBaseObj (void) : m_id(-1) { this.SetName(""); } ~CBaseObj (void) {}

MQL5 / C++
class="type">void SetID(const class="type">int id) { this.m_id=id; }
class=class="str">"cmt">//--- Return(class="num">1) name and(class="num">2) ID
class="type">class="kw">string Name(class="type">void) const { class="kw">return ::ShortArrayToString(this.m_name); }
class="type">int ID(class="type">void) const { class="kw">return this.m_id; }
class=class="str">"cmt">//--- Virtual methods of(class="num">1) comparing, (class="num">2) saving to file, (class="num">3) loading from file, (class="num">4) object type
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const;
class="kw">virtual class="type">bool Save(const class="type">int file_handle);
class="kw">virtual class="type">bool Load(const class="type">int file_handle);
class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return(ELEMENT_TYPE_BASE); }
class=class="str">"cmt">//--- (class="num">1) Return and(class="num">2) display the object description in the journal
class="kw">virtual class="type">class="kw">string Description(class="type">void);
class="kw">virtual class="type">void Print(class="type">void);
class=class="str">"cmt">//--- Constructor/destructor
CBaseObj(class="type">void) : m_id(-class="num">1) { this.SetName(""); }
~CBaseObj(class="type">void) {}

「对象序列化落盘的起手式」

把图形对象存进二进制文件,第一道关是先验文件句柄。句柄等于 INVALID_HANDLE 就直接返 false,避免后续写操作把数据甩到空句柄上引发运行时报错。 写入端用 0xFFFFFFFFFFFFFFFF(即 -1 的 long 表达)做数据块起始标记,FileWriteLong 返回值不等于 sizeof(long) 说明没写满 8 字节,也要立即退出。随后依次落盘对象类型、ID、名称数组,任何一步写字节数对不上就失败。 读回时要先核对起始标记:FileReadLong 读出来不是 -1 就说明文件指针错位或文件损坏,直接返 false。类型不匹配同样弃读,ID 和名称才接着载入。 下面这段是基类和颜色类的存读骨架,注意 CColor::Save 里句柄检查和标记写入与基类逻辑一致,复制时可省掉重复判断。

MQL5 / C++
class="type">bool CBaseObj::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-class="num">1)!=class="kw">sizeof(class="type">long))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the ID
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the name
   if(::FileWriteArray(file_handle,this.m_name)!=class="kw">sizeof(this.m_name))
      class="kw">return class="kw">false;

class=class="str">"cmt">//--- All is successful
   class="kw">return true;
  }

class="type">bool CBaseObj::Load(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileReadLong(file_handle)!=-class="num">1)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Load the object type
   if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type())
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Load the ID
   this.m_id=::FileReadInteger(file_handle,INT_VALUE);
class=class="str">"cmt">//--- Load the name
   if(::FileReadArray(file_handle,this.m_name)!=class="kw">sizeof(this.m_name))
      class="kw">return class="kw">false;

class=class="str">"cmt">//--- All is successful
   class="kw">return true;
  }

class="type">bool CColor::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Check the handle
   if(file_handle==INVALID_HANDLE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save data start marker - 0xFFFFFFFFFFFFFFFF
   if(::FileWriteLong(file_handle,-class="num">1)!=class="kw">sizeof(class="type">long))

把控件颜色状态落盘到文件

做 GUI 控件库时,颜色对象不能只活在内存里。要让 MT5 重启后界面状态不丢,就得把每个颜色状态的结构化字段写进二进制文件,这里用 FileWriteInteger 和 FileWriteArray 按固定顺序落盘。 基类的 Save 方法先写对象类型、颜色值、ID 和名称。类型与颜色都用 INT_VALUE 写入(4 字节),名称通过 FileWriteArray 直接拷 m_name 字节流;任何一步返回值不符预期就 return false,保证文件不会写半个对象。 CColor::Save 先调 CBaseObj::Save 继承父层数据,再单独补一行颜色整数。注意这里没有重写 ID/名称,说明颜色子类复用基类已落盘的标识,文件布局是「父层块 + 子层块」的拼接。 CColorElement 构造函数把四种交互态预设写死:Default=1、Focused=2、Pressed=3、Blocked=4,Current 态 ID 为 0。开 MT5 自己写个测试 EA,new 一个 CColorElement 后调 Save,用 Hex 编辑器打开产物,偏移 0 起应是类型(4B)+颜色(4B)+ID(4B)+名称数组,能直接核对状态机编号。 外汇与贵金属 GUI 工具涉及真实账户交互,控件状态错乱可能引发误下单,这类底层读写请在模拟盘充分验证。

MQL5 / C++
  class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the object type
   if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the class="type">color
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the ID
   if(::FileWriteInteger(file_handle,this.m_id,INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the name
   if(::FileWriteArray(file_handle,this.m_name)!=class="kw">sizeof(this.m_name))
      class="kw">return class="kw">false;

class=class="str">"cmt">//--- All is successful
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CColor::Save to file                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CColor::Save(const class="type">int file_handle)
  {
class=class="str">"cmt">//--- Save the parent object data
   if(!CBaseObj::Save(file_handle))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Save the class="type">color
   if(::FileWriteInteger(file_handle,this.m_color,INT_VALUE)!=INT_VALUE)
      class="kw">return class="kw">false;

class=class="str">"cmt">//--- All is successful
   class="kw">return true;
   }
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
class=class="str">"cmt">//| CColorControl::Constructor with setting the transparent colors of the object|
class=class="str">"cmt">//+-----------------------------------------------------------------------------+
CColorElement::CColorElement(class="type">void)
  {
   this.InitColors(clrNULL,clrNULL,clrNULL,clrNULL);
   this.m_default.SetName("Default"); this.m_default.SetID(class="num">1);
   this.m_focused.SetName("Focused"); this.m_focused.SetID(class="num">2);
   this.m_pressed.SetName("Pressed"); this.m_pressed.SetID(class="num">3);
   this.m_blocked.SetName("Blocked"); this.m_blocked.SetID(class="num">4);
   this.SetCurrentAs(COLOR_STATE_DEFAULT);
   this.m_current.SetName("Current");
   this.m_current.SetID(class="num">0);
  }
class="kw">public:
class=class="str">"cmt">//--- Return a new class="type">color
   class="type">color       NewColor(class="type">color base_color, class="type">int shift_red, class="type">int shift_green, class="type">int shift_blue);
class=class="str">"cmt">//--- Class initialization
   class="type">void        Init(class="type">void);
class=class="str">"cmt">//--- Initialize colors for different states

◍ 控件配色与边界矩形的初始化拆解

在 MT5 自定义面板的底层类里,CColorElement 用 Init() 一次性把四种交互态写死:Default=1、Focused=2、Pressed=3、Blocked=4,当前态 ID 固定为 0。这种硬编码 ID 的好处是后续状态机切换不用查表,直接比整数就行。 InitColors() 则按基准色 clr 自动推导其余态:聚焦态在 RGB 各通道减 20,按下态减 40,禁用态直接给 clrWhiteSmoke。若传入 clrNULL,聚焦和按下态也返回 clrNULL,不会瞎填颜色。 边界类 CBound 封装了 CRect 结构,提供 ResizeW / ResizeH / Resize 三个方法分别改宽、改高、同时改。做悬浮窗或按钮热区时,先 Resize 再绘制的顺序不能反,否则命中判定会偏一格。 下面这段是原文核心,逐行看一遍就能抄进自己的 EA 面板工程:

MQL5 / C++
class=class="str">"cmt">//| CColorControl::Class initialization                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CColorElement::Init(class="type">void)
  {
   this.m_default.SetName("Default"); this.m_default.SetID(class="num">1);
   this.m_focused.SetName("Focused"); this.m_focused.SetID(class="num">2);
   this.m_pressed.SetName("Pressed"); this.m_pressed.SetID(class="num">3);
   this.m_blocked.SetName("Blocked"); this.m_blocked.SetID(class="num">4);
   this.SetCurrentAs(COLOR_STATE_DEFAULT);
   this.m_current.SetName("Current");
   this.m_current.SetID(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//| CColorControl::Set the colors for all states based on the current one|
class=class="str">"cmt">//+----------------------------------------------------------------------+
class="type">void CColorElement::InitColors(const class="type">color clr)
  {
   this.InitDefault(clr);
   this.InitFocused(clr!=clrNULL ? this.NewColor(clr,-class="num">20,-class="num">20,-class="num">20) : clrNULL);
   this.InitPressed(clr!=clrNULL ? this.NewColor(clr,-class="num">40,-class="num">40,-class="num">40) : clrNULL);
   this.InitBlocked(clrWhiteSmoke);   
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Rectangular region class                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBound : class="kw">public CBaseObj
  {
class="kw">protected:
   CRect            m_bound;                                                                     class=class="str">"cmt">// Rectangular area structure
class="kw">public:
class=class="str">"cmt">//--- Change the bounding rectangular(class="num">1) width, (class="num">2) height and(class="num">3) size
   class="type">void             ResizeW(const class="type">int size)                       { this.m_bound.Width(size);                                      }
   class="type">void             ResizeH(const class="type">int size)                       { this.m_bound.Height(size);                                     }
   class="type">void             Resize(const class="type">int w,const class="type">int h)               { this.m_bound.Width(w); this.m_bound.Height(h);                 }

class=class="str">"cmt">//--- Set(class="num">1) X, (class="num">2) Y and(class="num">3) both coordinates of the bounding rectangle

「矩形边界的 setter 与坐标读取」

在 MT5 自定义类里,边界框(bounding rectangle)的写入和读取通常成对出现。下面这组方法把左上角坐标、整体位移、以及宽高回报都封装在 m_bound 成员上,调用方不需要直接接触底层结构体。 SetX / SetY / SetXY 只负责设定绝对位置:前两个单独改 left 或 top,第三个用 LeftTop(x,y) 一次性落点。回测面板若要在 1920×1080 分辨率下贴右边缘,设 X(1800) 后宽度超过 120 就可能出界,这类数值得实测。 Move 与 Shift 的区别在于语义:Move(x,y) 是移动到指定坐标,Shift(dx,dy) 是相对当前位置偏移。做拖拽类控件时,鼠标移动事件里用 Shift 累加、窗口重绘时用 Move 定点,逻辑更干净。 读取侧 X()、Y() 直接返回 left / top,Width()、Height() 委托给 m_bound 的方法。注意这四个都是 const 方法,意味着它们不会改动对象状态,可以放心在只读上下文(如 OnChartEvent 的 hover 判断)里高频调用。

MQL5 / C++
class="type">void SetX(const class="type">int x) { this.m_bound.left=x; }
class="type">void SetY(const class="type">int y) { this.m_bound.top=y; }
class="type">void SetXY(const class="type">int x,const class="type">int y){ this.m_bound.LeftTop(x,y); }

class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) shift the bounding rectangle by the specified coordinates/offset size
class="type">void Move(const class="type">int x,const class="type">int y) { this.m_bound.Move(x,y); }
class="type">void Shift(const class="type">int dx,const class="type">int dy){ this.m_bound.Shift(dx,dy); }

class=class="str">"cmt">//--- Returns the object coordinates, dimensions, and boundaries
class="type">int X(class="type">void) const { class="kw">return this.m_bound.left; }
class="type">int Y(class="type">void) const { class="kw">return this.m_bound.top; }
class="type">int Width(class="type">void) const { class="kw">return this.m_bound.Width(); }
class="type">int Height(class="type">void) const { class="kw">return this.m_bound.Height(); }

矩形区域类的边界与画布基类收口

CBound 类把矩形区域的右边界和下边界做了减一处理:当 Width() 大于 0 时 Right() 返回 m_bound.right-1,Height() 大于 0 时 Bottom() 返回 m_bound.bottom-1。这个减一动作是为了让像素坐标从「含尾」转成「不含尾」,在 MT5 里用鼠标命中测试时才不会多算最外一圈像素。 Contains() 直接复用 m_bound.Contains(x,y),对外暴露光标是否落在区域内的布尔值;Type() 写死返回 ELEMENT_TYPE_RECTANGLE_AREA,相当于给对象一个稳定身份标签,方便后续在 CObject 链表里做类型分流。 构造函数提供了两种入口:无参版用 ZeroMemory 把 m_bound 清成全零,带参版接收 x、y、w、h 后依次调 SetXY 与 Resize 完成初始化;析构同样 ZeroMemory,避免残留坐标被误读。 CCanvasBase 作为图形元素画布基类,private 里挂了 m_chart_mouse_wheel_flag 与 m_chart_mouse_move_flag 两个布尔开关,分别控制是否向图表推送滚轮滚动与光标移动消息。想验证这套机制,可在 MT5 自建 EA 里继承 CBound 并打印 Right()/Bottom() 的实际返回值,看减一逻辑是否如预期生效。外汇与贵金属图表交互开发属高风险环境,坐标偏差可能导致信号误触。

MQL5 / C++
class="type">int Right(class="type">void) const { class="kw">return this.m_bound.right-(this.m_bound.Width() >class="num">0 ? class="num">1 : class="num">0);}
class="type">int Bottom(class="type">void) const { class="kw">return this.m_bound.bottom-(this.m_bound.Height()>class="num">0 ? class="num">1 : class="num">0);}
class=class="str">"cmt">//--- Returns the flag indicating whether the cursor is inside the area
class="type">bool Contains(const class="type">int x,const class="type">int y) const { class="kw">return this.m_bound.Contains(x,y); }
class=class="str">"cmt">//--- Return the object description
class="kw">virtual class="type">class="kw">string Description(class="type">void);
class=class="str">"cmt">//--- Virtual methods of(class="num">1) comparing, (class="num">2) saving to file, (class="num">3) loading from file, (class="num">4) object type
class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const;
class="kw">virtual class="type">bool Save(const class="type">int file_handle);
class="kw">virtual class="type">bool Load(const class="type">int file_handle);
class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return(ELEMENT_TYPE_RECTANGLE_AREA); }
class=class="str">"cmt">//--- Constructors/destructor
CBound(class="type">void) { ::ZeroMemory(this.m_bound); }
CBound(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h) { this.SetXY(x,y); this.Resize(w,h); }
~CBound(class="type">void) { ::ZeroMemory(this.m_bound); }
};
class CCanvasBase : class="kw">public CBaseObj
{
class="kw">private:
 class="type">bool m_chart_mouse_wheel_flag; class=class="str">"cmt">// Flag for sending mouse wheel scroll messages
 class="type">bool m_chart_mouse_move_flag; class=class="str">"cmt">// Flag for sending mouse cursor movement messages
交给小布看控件事件流
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件交互是否按预期触发,把重复劳动交给小布,你专注决策。

常见问题

基类集中了保存、加载、打印等通用方法,优化后新控件只需写差异逻辑,避免每个类重复相同代码。
它们既是视图的绘制单元,也通过图表事件接收交互,承担了控制器连接视图与模型的部分职责。
可以,小布盯盘的 AIGC 模块能呈现品种页上的交互诊断,帮你确认控件在悬停、点击时是否按设定变色或触发。
在普通按钮左侧绘图标、右侧放文字,调整字符串参数即可在悬停或点击时变色,无需新建独立类。
需新建 Indicators\tables\controls 路径并放入文件,同时基类要预声明所有列表内对象的类以支持正确序列化。