MQL 作为 MQL 程序图形界面的标记工具。 第二部分·进阶篇
🧩

MQL 作为 MQL 程序图形界面的标记工具。 第二部分·进阶篇

(2/3)· 从私有到保护、RTTI 调试到事件处理,拆解标准库分支改造的 4 个关键落点

进阶 第 2/3 篇
直接改标准库源码的人,多半在多个 EA 间踩过控件 ID 冲突的坑。把类整份拷成分支再改,比原地打补丁更利于后续升级。本篇顺着这个分支思路,看怎么让界面真正可扩展。

◍ 用缓存统一管界面样式

StdLayoutCache 把元素集中处理,顺带能把样式也收口:颜色、字体、缩进这类规则只写一处,所有控件自动继承,不用逐个显式赋值。若完全不用样式,基类 stub 方法直接返回 NULL,业务代码零改动;想集中管样式,就从 LayoutStyleable 派生,接口只有一個 apply。 apply 会在两个时机被调:初始化前 STYLE_PHASE_BEFORE_INIT,以及注册进容器后 STYLE_PHASE_AFTER_INIT。分两阶段是因为样式目标有冲突可能——有些控件要设独立属性,优先级高于公共样式。初始化阶段控件还是空壳,布局未落;注册阶段属性全齐,可基于它们再改样式。 典型现象:所有标记“只读”的区域倾向显示灰色,但“只读”标志只在布局时赋给控件,所以第一阶段不合适,必须放第二阶段;其余区域则在第一阶段设默认色,留给布局语言做选择性覆盖。同样思路也能搬去 MQL 程序界面的集中本地化。 下面这段是 StdLayoutCache 里样式相关的骨架代码,两阶段调用就藏在 init 与析构的判空逻辑里。

MQL5 / C++
<span class="keyword">class="kw">virtual</span> LayoutStyleable&lt;C&gt; *getStyler() <span class="keyword">class="kw">const</span>
    {
      <span class="keyword">class="kw">return</span> <span class="macro">NULL</span>;
    }
  <span class="keyword">enum</span> STYLER_PHASE
  {
    STYLE_PHASE_BEFORE_INIT,
    STYLE_PHASE_AFTER_INIT
  };
  
  <span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> C&gt;
  <span class="keyword">class </span>LayoutStyleable
  {
    <span class="keyword">class="kw">public</span>:
      <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> apply(C *control, <span class="keyword">class="kw">const</span> STYLER_PHASE phase) {};
  };
      <span class="keyword">if</span>(cacher != <span class="macro">NULL</span>)
      {
        LayoutStyleable&lt;C&gt; *styler = cacher.getStyler();
        <span class="keyword">if</span>(styler != <span class="macro">NULL</span>)
        {
          styler.apply(object, STYLE_PHASE_BEFORE_INIT);
        }
      }

「把 UI 事件引到缓存里处理」

缓存的第二个职责是接管界面事件。LayoutCache 里先放一个 onEvent 的 stub:virtual bool onEvent(const int event, C *control) { return false; },派生类想覆盖就覆盖,不覆盖也不影响编译。 事件代码由函数库自己定义,要让缓存真正动起来,得用一组拦截宏,把图表自定义事件重定向进缓存对象。宏里按 lparam 取反后的标识符在缓存中查找元素,再喂给 onEvent,这就是事件落点的核心路径。 搜索这一步其实可以省。元素存进缓存时,当前缓存大小减一就是它的索引,例如 button1index = cache.cacheSize() - 1,把索引记在窗口类里,后面用 ON_EVENT_LAYOUT_INDEX 按索引直接绑定处理函数,省掉每次查找的开销。 另一种写法是不经过缓存,让控件自己实现 Notifiable 接口。模板参数传具体控件类(如 CButton),父对象可以是对话框也可以是任意指针。配套宏有两条:ON_EVENT_LAYOUT_CTRL_ANY 允许最后一个参数传随机对象,ON_EVENT_LAYOUT_CTRL_DLG 则固定把 this 当对话框传进去。实盘里点按钮重置状态这类动作,用衍生类覆写 onEvent 就能直接搞定。

MQL5 / C++
class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, C *control)
    {
      class="kw">return class="kw">false;
    }
  EVENT_MAP_BEGIN(Dialog)
    ON_EVENT(ON_CLICK, m_button1, OnClickButton1)
    ...
  EVENT_MAP_END(AppDialog)
  class="macro">#define ON_EVENT_LAYOUT_ARRAY(event, cache)  if(id == (event + CHARTEVENT_CUSTOM) && cache.onEvent(event, cache.get(-lparam))) { class="kw">return true; }
          _layout<CButton> button1("Button");
          button1index = cache.cacheSize() - class="num">1;
  class="macro">#define ON_EVENT_LAYOUT_INDEX(event, cache, controlIndex, handler)  if(id == (event + CHARTEVENT_CUSTOM) && lparam == cache.get(controlIndex).Id()) { handler(); class="kw">return(true); }
  class="kw">template<class="kw">typename C>
  class Notifiable: class="kw">public C
  {
    class="kw">public:
      class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, class="type">void *parent) = class="num">0;
  };
  class NotifiableButton: class="kw">public Notifiable<CButton>
  {
    class="kw">public:
      class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, class="type">void *anything) class="kw">override
      {
        this.StateFlagsReset(class="num">7);
        class="kw">return true;
      }
  };
  class="macro">#define ON_EVENT_LAYOUT_CTRL_ANY(event, cache, type, anything)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = class="kw">dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, anything)) { class="kw">return true; }}
  class="macro">#define ON_EVENT_LAYOUT_CTRL_DLG(event, cache, type)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = class="kw">dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, &this)) { class="kw">return true; }}

用缓存和样式类重做控件对话框布局

演示项目里的 CControlsDialog 基于标准库控件类型,原项目光是创建控件的方法就有 17 个,还靠复合条件运算符互相调用。实操里直接砍掉这些碎方法,只留一个 CreateLayout,再把生成出来的控件丢进缓存,顺手挂一个样式类统一外观。 缓存类里声明了 onEvent 处理器,通过事件映射连上后,它会把消息转给父窗口,父窗口像之前那个版本一样把内容刷到信息字段。样式类则给所有元素设相同字段,按钮强制用 Arial Black 这种非标准字体,只读 CEdit 用灰色底显示——目前只有一个只读框,但以后加别的也会自动套用。 窗口在构造和析构时分别 new 和 delete 缓存,创建时把 this 传进去,保证后续能回传反馈。CreateLayout 看着长,去掉注释其实一屏装得下,逻辑并不绕。 开头先 setCache 激活缓存,再描述主容器 CControlsDialog,因为传了 this 所以不进缓存。接着隐式建一个垂直 CBox 做客户区,存到 m_main,调窗口大小时要调它的 Pack;不是“橡皮”对话框就不用管。客户区零边距、全方向对齐,填满整窗。 再往下一层是贴顶的编辑行容器,里面只有一个显式保留的只读 m_edit。三个按钮那行用了 WND_ALIGN_CONTENT 这个非标准对齐值:CBox 的 AdjustFlexControls 会算固定尺寸控件的总占用,剩下的空间按比例分给没绑边的控件。两个都没绑边的按钮会对半分。这套模式方便,但别在交错容器上乱用,单次计算会让内部对齐不确定,所以布局类抛了 ON_LAYOUT_REFRESH 让窗口重算。 三个按钮各自用大括号包住,按顺序 1、2、3 加进行里;省略个人模块就得反序加,因为析构是反的。第三行匿名建容器存缓存,塞了微调器和日历。最后一行填满剩余区,分两列,用亮色方便看,列内容靠助手方法 createSubList 填,改第二个参数就能换列顺序。 布局收尾调 Pack 或 SelfAdjustment 响应橡皮窗。事件映射里 ON_EVENT_LAYOUT_CTRL_DLG 绑 NotifyableButton 单击,ON_EVENT_LAYOUT_INDEX 按缓存索引发,ON_EVENT_LAYOUT_ARRAY 则把单击发给 lparam 匹配的任意缓存元素。红色调试框用 LAYOUT_BOX_DEBUG 宏关掉。外汇和贵金属 GUI 自动化高风险,参数改动前建议在 MT5 策略测试器先跑一遍。

MQL5 / C++
class MyStdLayoutCache: class="kw">public StdLayoutCache
{
class="kw">protected:
MyLayoutStyleable styler;
CControlsDialog *parent;
class="kw">public:
MyStdLayoutCache(CControlsDialog *owner): parent(owner) {}
class="kw">virtual StdLayoutStyleable *getStyler() class="kw">const class="kw">override
{
class="kw">return (StdLayoutStyleable *)&styler;
}
class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, CWnd *control) class="kw">override
{
if(control != NULL)
{
parent.SetCallbackText(__FUNCTION__ + " " + control.Name());
class="kw">return true;
}
class="kw">return class="kw">false;
}
};
class MyLayoutStyleable: class="kw">public StdLayoutStyleable
{
class="kw">public:
class="kw">virtual class="type">void apply(CWnd *control, class="kw">const STYLER_PHASE phase) class="kw">override
{
CButton *button = class="kw">dynamic_cast<CButton *>(control);
if(button != NULL)
{
if(phase == STYLE_PHASE_BEFORE_INIT)
{
button.Font("Arial Black");
}
}
else
{
CEdit *edit = class="kw">dynamic_cast<CEdit *>(control);
if(edit != NULL && edit.ReadOnly())
{
if(phase == STYLE_PHASE_AFTER_INIT)
{
edit.ColorBackground(clrLightGray);
}
}
}
if(phase == STYLE_PHASE_BEFORE_INIT)
{
control.Margins(DEFAULT_MARGIN);
}
}
};
class CControlsDialog: class="kw">public AppDialogResizable
{
class="kw">private:
...
MyStdLayoutCache *cache;
class="kw">public:
CControlsDialog(class="type">void)
{
cache = new MyStdLayoutCache(&this);
}

◍ 用缓存布局把控件堆进对话框

在 MT5 自定义面板里,CControlsDialog::CreateLayout 这套写法把控件声明全部塞进局部作用域,靠 StdLayoutBase::setCache 把隐式对象存进 cache,离开括号就自动析构但指针已留好。m_main = clientArea.get() 这类调用说明你随时能从缓存里拿回对象地址,不必自己管生命周期。 看 spin 那段:spin["min"] <= 10、spin["max"] <= 1000、spin["value"] <= 100,注释直接点明 value 必须在 limits 之后设,这是 SpinEdit 的硬规则,顺序反了值不生效。button3 <= true 则是按钮默认布尔属性走 Locking,不是 ReadOnly——和 edit 的默认行为正好反过来。 列表区用 SymbolsItemGenerator 模板类往 ListView 灌品种名:SymbolName((int)index, true) 配合 SymbolsTotal(true) 做终止判断,每调一次 addItemTo 就 index++,直到覆盖当前可见市场所有符号。list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>() 这种右值注入写法,省掉你手动循环 AddItem 的样板代码。 事件绑定走 EVENT_MAP_BEGIN/CACHE/INDEX/ARRAY 三层宏,button1index = cache.cacheSize() - 1 这行是关键:隐式布局对象的索引就是缓存压栈顺序,错一个号就绑到别的控件上。开 MT5 把这段抄进你的 panel 工程,改 GROUP_WIDTH 和 LIST_HEIGHT 两个常量就能直观看到布局错位或自适应。

MQL5 / C++
class="type">bool CControlsDialog::CreateLayout(class="kw">const class="type">long chart, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int subwin, class="kw">const class="type">int x1, class="kw">const class="type">int y1, class="kw">const class="type">int x2, class="kw">const class="type">int y2)
{
StdLayoutBase::setCache(cache); class=class="str">"cmt">// assign the cache object to store implicit objects
{
_layout<CControlsDialog> dialog(this, name, x1, y1, x2, y2);
{
class=class="str">"cmt">// example of implicit object in the cache
_layout<CBox> clientArea("main", ClientAreaWidth(), ClientAreaHeight(), LAYOUT_STYLE_VERTICAL);
m_main = clientArea.get(); class=class="str">"cmt">// we can get the pointer to the object from cache(if required)
clientArea <= WND_ALIGN_CLIENT <= class="num">0.0; class=class="str">"cmt">// class="type">class="kw">double type is important
{
class=class="str">"cmt">// another implicit container(we need no access it directly)
_layout<CBox> editRow("editrow", ClientAreaWidth(), EDIT_HEIGHT * class="num">1.5, (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH));
{
class=class="str">"cmt">// for editboxes class="kw">default boolean class="kw">property is ReadOnly
_layout<CEdit> edit(m_edit, "Edit", ClientAreaWidth(), EDIT_HEIGHT, true);
}
}
{
_layout<CBox> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * class="num">1.5);
buttonRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
{ class=class="str">"cmt">// class="num">1
_layout<CButton> button1("Button1");
button1index = cache.cacheSize() - class="num">1;
button1["width"] <= BUTTON_WIDTH;
button1["height"] <= BUTTON_HEIGHT;
} class=class="str">"cmt">// class="num">1
{ class=class="str">"cmt">// class="num">2
_layout<NotifiableButton> button2("Button2", BUTTON_WIDTH, BUTTON_HEIGHT);
} class=class="str">"cmt">// class="num">2
{ class=class="str">"cmt">// class="num">3
_layout<CButton> button3(m_button3, "Button3", BUTTON_WIDTH, BUTTON_HEIGHT, "Locked");
button3 <= true; class=class="str">"cmt">// for buttons class="kw">default boolean class="kw">property is Locking
} class=class="str">"cmt">// class="num">3
}
{
_layout<CBox> spinDateRow("spindaterow", ClientAreaWidth(), BUTTON_HEIGHT * class="num">1.5);
spinDateRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
{
_layout<SpinEditResizable> spin(m_spin_edit, "SpinEdit", GROUP_WIDTH, EDIT_HEIGHT);
spin["min"] <= class="num">10;
spin["max"] <= class="num">1000;
spin["value"] <= class="num">100; class=class="str">"cmt">// can set value only after limits(this is how SpinEdits work)
}
{
_layout<CDatePicker> date(m_date, "Date", GROUP_WIDTH, EDIT_HEIGHT, TimeCurrent());
}
}
{
_layout<CBox> listRow("listsrow", ClientAreaWidth(), LIST_HEIGHT);
listRow["top"] <= (class="type">int)(EDIT_HEIGHT * class="num">1.5 * class="num">3);
listRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_CLIENT);
(listRow <= clrMagenta)["border"] <= clrBlue;
createSubList(&m_lists_column1, LIST_OF_OPTIONS);
createSubList(&m_lists_column2, LIST_LISTVIEW);
class=class="str">"cmt">// or vice versa(changed order gives swapped left/right side location)
class=class="str">"cmt">// createSubList(&m_lists_column1, LIST_LISTVIEW);
class=class="str">"cmt">// createSubList(&m_lists_column2, LIST_OF_OPTIONS);
}
}
}
class=class="str">"cmt">// m_main.Pack();
SelfAdjustment();
class="kw">return true;
}
class="kw">template<class="kw">typename T>
class ItemGenerator
{
class="kw">public:
class="kw">virtual class="type">bool addItemTo(T *object) = class="num">0;
};
class="kw">template<class="kw">typename T>
class SymbolsItemGenerator: class="kw">public ItemGenerator<T>
{
class="kw">protected:
class="type">long index;
class="kw">public:
SymbolsItemGenerator(): index(class="num">0) {}
class="kw">virtual class="type">bool addItemTo(T *object) class="kw">override
{
object.AddItem(SymbolName((class="type">int)index, true), index);
index++;
class="kw">return index < SymbolsTotal(true);
}
};
_layout<ListViewResizable> list(m_list_view, "ListView", GROUP_WIDTH, LIST_HEIGHT);
list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>();
EVENT_MAP_BEGIN(CControlsDialog)
...
ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, NotifiableButton)
ON_EVENT_LAYOUT_INDEX(ON_CLICK, cache, button1index, OnClickButton1)
ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
EVENT_MAP_END(AppDialogResizable)
让小布替你跑这套
这些控件库分支的调试与布局诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到结构异常提示,你专注决策逻辑。

常见问题

受保护级别允许派生类重写和扩展标准控件行为,是实现布局系统集成的必要前提,否则无法做定制化子类。
它用 RTTI 宏填充真实派生类名称,调试器通过基类指针解引用时能直接看到实际对象类型,方便排查界面元素归属。
CAppDialog 分配标识符时纳入窗口的 instance_id,可隔离不同实例的控件命名空间,避免冲突。
可以,小布盯盘内置的 AIGC 能呈现品种页里的 GUI 结构诊断,减少手动翻调试器的重复劳动。
原实现在双向拉伸时无法保证平移到预设区域边界,重写后对齐行为符合布局系统的预期。