MQL 作为 MQL 程序图形界面的标记工具。 第二部分·进阶篇
(2/3)· 从私有到保护、RTTI 调试到事件处理,拆解标准库分支改造的 4 个关键落点
◍ 用缓存统一管界面样式
StdLayoutCache 把元素集中处理,顺带能把样式也收口:颜色、字体、缩进这类规则只写一处,所有控件自动继承,不用逐个显式赋值。若完全不用样式,基类 stub 方法直接返回 NULL,业务代码零改动;想集中管样式,就从 LayoutStyleable 派生,接口只有一個 apply。 apply 会在两个时机被调:初始化前 STYLE_PHASE_BEFORE_INIT,以及注册进容器后 STYLE_PHASE_AFTER_INIT。分两阶段是因为样式目标有冲突可能——有些控件要设独立属性,优先级高于公共样式。初始化阶段控件还是空壳,布局未落;注册阶段属性全齐,可基于它们再改样式。 典型现象:所有标记“只读”的区域倾向显示灰色,但“只读”标志只在布局时赋给控件,所以第一阶段不合适,必须放第二阶段;其余区域则在第一阶段设默认色,留给布局语言做选择性覆盖。同样思路也能搬去 MQL 程序界面的集中本地化。 下面这段是 StdLayoutCache 里样式相关的骨架代码,两阶段调用就藏在 init 与析构的判空逻辑里。
<span class="keyword">class="kw">virtual</span> LayoutStyleable<C> *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><<span class="keyword">class="kw">typename</span> C> <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<C> *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 就能直接搞定。
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 策略测试器先跑一遍。
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 两个常量就能直观看到布局错位或自适应。
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)