MQL5 MVC架构中表格视图与控制器组件:简单控件·进阶篇
(2/3)· 基类优化后,文本标签与各类按钮如何成为复合界面元素的积木
◍ 基类对象怎么管 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) {}
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 里句柄检查和标记写入与基类逻辑一致,复制时可省掉重复判断。
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 工具涉及真实账户交互,控件状态错乱可能引发误下单,这类底层读写请在模拟盘充分验证。
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 面板工程:
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 判断)里高频调用。
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() 的实际返回值,看减一逻辑是否如预期生效。外汇与贵金属图表交互开发属高风险环境,坐标偏差可能导致信号误触。
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