MQL 作为 MQL 程序图形界面的标记工具(第三部)。 窗体设计师·进阶篇
(2/3)· 动态创建控件后如何高效排版与调参?本文拆解编辑器双窗口协作逻辑
用观察者自动锁死无效配置的按钮
在 MT5 面板开发里,最烦的是用户没填完参数就点“应用”,把脏数据塞进 PropertySet。与其在点击事件里写一堆判断,不如让控件状态自己跟着依赖属性变:依赖源一变,按钮自动 Enable/Disable。 核心是一个抽象类 EnableStateMonitorBase,它把受监控的控件指针放在 control 字段,把影响状态的变更源塞进 sources 数组。子类必须填 sources,并自己实现 isEnabled 来决定锁还是放。调用 attach 把观察者绑到具体控件时,它会一次性订阅 sources 里所有源,之后源一变就走 notify 重算状态。 实际例子是 Inspector 对话框的 Apply 按钮。若对象名没填、类型没选(等于 -1),或者名字里带了标准库给客户区预留的 "Client" 字符串,按钮就必须锁住。ApplyButtonStateMonitor 用两个源——name 和 type——在 isEnabled 里做这三道检查,满足才放行。 对话框构造函数里 new 一个 monitor,把 props.name 和 props.type 的 StdValue 引用喂进去;布局阶段 EditProperty、ComboBoxProperty 通过 _layout 的 attach 绑到对应 StdValue,观察者再绑到 Apply 按钮。用户改任一字段,NotifiableProperty 的 ON_CHANGE / ON_END_EDIT 会把值写回 PropertySet 并通知 monitor,按钮状态秒级翻转,不用手动刷。 外汇与贵金属 EA 面板属于高风险场景,这类自动锁按钮逻辑只解决交互脏数据,不替代策略风控。
class="kw">template<class="kw">typename C> class EnableStateMonitorBase: class="kw">public StateMonitor { class="kw">protected: Publisher *sources[]; C *control; class="kw">public: EnableStateMonitorBase(): control(NULL) {} class="kw">virtual class="type">void attach(C *c) { control = c; for(class="type">int i = class="num">0; i < ArraySize(sources); i++) { if(control) { sources[i].subscribe(&this); } else { sources[i].unsubscribe(&this); } } } class="kw">virtual class="type">bool isEnabled(class="type">void) = class="num">0; }; class EnableStateMonitor: class="kw">public EnableStateMonitorBase<CWnd> { class="kw">public: EnableStateMonitor() {} class="type">void notify(class="type">void *sender) class="kw">override { if(control) { if(isEnabled()) { control.Enable(); } else { control.Disable(); } } } }; class ApplyButton: 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 *parent) class="kw">override { if(event == ON_CLICK) { ... } } }; class ApplyButtonStateMonitor: class="kw">public EnableStateMonitor { class=class="str">"cmt">// what&class="macro">#x27;s required to detect Apply button state class="kw">const class="type">int NAME; class="kw">const class="type">int TYPE; class="kw">public: ApplyButtonStateMonitor(StdValue<class="type">class="kw">string> *n, StdValue<class="type">int> *t): NAME(class="num">0), TYPE(class="num">1) { ArrayResize(sources, class="num">2); sources[NAME] = n; sources[TYPE] = t; } class="kw">virtual class="type">bool isEnabled(class="type">void) class="kw">override { StdValue<class="type">class="kw">string> *name = sources[NAME]; StdValue<class="type">int> *type = sources[TYPE]; class="kw">return StringLen(~name) > class="num">0 && ~type != -class="num">1 && ~name != "Client"; } }; class InspectorDialog: class="kw">public AppDialogResizable { class="kw">private: PropertySet props; ApplyButtonStateMonitor *applyMonitor; class="kw">public: InspectorDialog::InspectorDialog(class="type">void) { ... applyMonitor = new ApplyButtonStateMonitor(&props.name, &props.type); } ... _layout<EditProperty> edit("NameEdit", BUTTON_WIDTH, BUTTON_HEIGHT, ""); edit.attach(&props.name); ... _layout<ComboBoxProperty> combo("TypeCombo", BUTTON_WIDTH, BUTTON_HEIGHT); combo.attach(&props.type); ... _layout<ApplyButton> button1("Apply", BUTTON_WIDTH, BUTTON_HEIGHT);
◍ 用模板把控件和数值绑起来
在 MT5 面板里关掉某个开关,只需要把对应按钮的 enable 状态置为 false,再把监控对象挂上去即可。下面这段骨架演示了如何用模板类把任意 StdValue 绑进布局对象,省去每类控件写一遍绑定函数的麻烦。 核心在于 _layout 继承自 StdLayoutBase,并通过 attach 模板方法把外部传入的 StdValue 指针交给内部 object 去 bind。这样同一套布局代码可以复用于按钮、输入框等不同控件,编译期决定类型,运行期零额外开销。 实际验证时,可在 OnInit 里 new 一个 StdValue<bool> 传给 button1.get(),断点跟到 ((T *)object).bind(v) 确认地址写入;外汇与贵金属 EA 涉及界面交互时务必注意终端崩溃风险,误绑空指针可能直接卡死图表。
button1["enable"] <= class="kw">false; applyMonitor.attach(button1.get()); class="kw">template<class="kw">typename T> class _layout: class="kw">public StdLayoutBase { ... class="kw">template<class="kw">typename V> class="type">void attach(StdValue<V> *v) { ((T *)object).bind(v); } ... };
「用间接寻址绕过线性搜索」
MQL5 标准库的 StdLayoutCache 在事件触发时,靠 lparam 里的唯一编号去遍历缓存数组做线性搜索。控件一多,这种 O(n) 扫描会拖慢 CAppDialog::Run 里的事件分发,尤其 Inspector 这类塞满属性行的窗口。 实际图表对象数远多于缓存里的“逻辑控件”:一个控件往往由框架、标题、最小化按钮等多个低级对象拼成,所以对象 Id 减去 m_instance_id 后,并不能直接当缓存下标用。 做法是给 MyStdLayoutCache 加一个 buildIndex 方法,分配大小等于窗口对象总数的索引数组,把缓存里真实控件的序号写进去。之后事件携带的 lparam 取负再减 start,就能经 index[] 做一次间接寻址直插目标,跳过遍历。 buildIndex 必须在 CAppDialog::Run 分配完唯一编号、但 OnInit 应答结束前填充。标准库 Run 不是虚函数,要在 InspectorDialog 里重写 Run,调完父类 Run 成功后再 cache.buildIndex()。 事件映射里 ON_EVENT_LAYOUT_CTRL_DLG 把 ON_END_EDIT、ON_CHANGE、ON_CLICK 等绑到 cache 的对应处理方法;ApplyButton 继承 Notifiable,用户改完属性点按钮,onEvent 里取 inspector.getProperties().flatten() 丢给 designer.inject() 并 ChartRedraw()。外汇/贵金属面板开发涉及实时交互,MT5 自定义控件高风险,建议在策略测试器外先用简单对话框验证索引逻辑。
EVENT_MAP_BEGIN(InspectorDialog) ON_EVENT_LAYOUT_CTRL_DLG(ON_END_EDIT, cache, EditProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, SpinEditProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditPropertyShort) ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxColorProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, AlignCheckGroupProperty) ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, ApplyButton) ... ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) class=class="str">"cmt">// class="kw">default (stub) EVENT_MAP_END(AppDialogResizable) class="type">bool InspectorDialog::Run(class="type">void) { class="type">bool result = AppDialogResizable::Run(); if(result) { cache.buildIndex(); } class="kw">return result; } class MyStdLayoutCache: class="kw">public StdLayoutCache { class="kw">protected: InspectorDialog *parent; class=class="str">"cmt">// fast access class="type">int index[]; class="type">int start; class="kw">public: MyStdLayoutCache(InspectorDialog *owner): parent(owner) {} class="type">void buildIndex() { start = parent.GetInstanceId(); class="type">int stop = class="num">0; for(class="type">int i = class="num">0; i < cacheSize(); i++) { class="type">int id = (class="type">int)get(i).Id(); if(id > stop) stop = id; } ArrayResize(index, stop - start + class="num">1); ArrayInitialize(index, -class="num">1); for(class="type">int i = class="num">0; i < cacheSize(); i++) { CWnd *wnd = get(i); index[(class="type">int)(wnd.Id() - start)] = i; } ... }; class="kw">virtual CWnd *get(class="kw">const class="type">long m) class="kw">override { if(m < class="num">0 && ArraySize(index) > class="num">0) { class="type">int offset = (class="type">int)(-m - start); if(offset >= class="num">0 && offset < ArraySize(index)) { class="kw">return StdLayoutCache::get(index[offset]); } } class="kw">return StdLayoutCache::get(m); } class ApplyButton: 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 *parent) class="kw">override { if(event == ON_CLICK) { Properties p = inspector.getProperties().flatten(); designer.inject(p); ChartRedraw(); class="kw">return true; } class="kw">return class="kw">false; }; }; PropertySet *getProperties(class="type">void) class="kw">const { class="kw">return (PropertySet *)&props; }
设计器里的新元素注入与点击选中逻辑
Designer 的 inject 方法本质是把目标容器压入布局堆栈,再按传入属性生成 GUI 元素。窗体里用鼠标点选就能切换插入上下文,同一次点击还会把所选元素属性抛给 Inspector,实现已创建对象的编辑。设计器靠元素名称和类型判断用户是想新增还是改旧:DefaultStdLayoutCache 里若已有该组合,就走编辑分支。 变量 cache 在 DesignerForm 中定义为 StdLayoutCache 派生的 DefaultStdLayoutCache 对象。StdLayoutCache.get 按名查对象,查不到就是新对象,此时算法调用 getSelected 检测用户当前选中的容器;我们实现的控件只能放进 CBox 容器,若没选中容器,就由 findParent 回溯父容器当目标。定位插入点后,外部模块建 _layout 对象并内部以字符串生成控件。 模板对象 _layout 用 MQL 静态标记描述,重载运算符 <= 来填属性,比如 CBoxH 的 HORIZONTAL_ALIGN、文本或轮转器的 ENUM_ALIGN_MODE;缩进、文本、颜色等交给 applyProperties。缓存命中时,助手 update 把 props 结构里的宽高、对齐、边距搬到 ptr 上。 窗体里的选中靠缓存对象的 onEvent 处理,StdLayoutCache 用 ON_EVENT_LAYOUT_ARRAY 宏映射图表事件,全部单击转发到派生类 onEvent。可见元素加红色边框(ColorBorder)作高亮:先清掉旧选的边框色 CONTROLS_BUTTON_COLOR_BORDER,再把新点中的指针存进 selected,发 ON_LAYOUT_SELECTION 事件,Inspector 的 OnRemoteSelection 拦截后通过标准 API 读全部属性。 为识别类型,底层 CWnd 带 _rtti 变量,所有派生类填特殊类名。Inspector 用 StdValue<string> 的反向链接 backlink() 写控件,内部改值不触发用户事件,因此还要手动写 PropertySet(props.name)。复合控件如 CCheckGroup 不适合重写虚修改方法,所以采用这种务实做法。
class="type">void inject(Properties &props) { CWnd *ptr = cache.get(props.name); if(ptr != NULL) { ... } else { CBox *box = class="kw">dynamic_cast<CBox *>(cache.getSelected()); if(box == NULL) box = cache.findParent(cache.getSelected()); if(box) { CWnd *added; StdLayoutBase::setCache(cache); { _layout<CBox> injectionPanel(box, box.Name()); { AutoPtr<StdLayoutBase> base(getPtr(props)); added = (~base).get(); added.Id(rand() + ((class="type">long)rand() << class="num">32)); } } box.Pack(); cache.select(added); } } AutoPtr<StdLayoutBase> base(getPtr(props)); StdLayoutBase *getPtr(class="kw">const Properties &props) { class="kw">switch(props.type) { case _BoxH: { _layout<CBoxH> *temp = applyProperties(new _layout<CBoxH>(props.name, props.width, props.height), props); temp <= (HORIZONTAL_ALIGN)props.style; class="kw">return temp; } case _Button: class="kw">return applyProperties(new _layout<CButton>(props.name, props.width, props.height), props); case _Edit: { _layout<CEdit> *temp = applyProperties(new _layout<CEdit>(props.name, props.width, props.height), props); temp <= (ENUM_ALIGN_MODE)LayoutConverters::style2textAlign(props.style); class="kw">return temp; } case _SpinEdit: { _layout<SpinEditResizable> *temp = applyProperties(new _layout<SpinEditResizable>(props.name, props.width, props.height), props); temp["min"] <= class="num">0; temp["max"] <= DUMMY_ITEM_NUMBER; temp["value"] <= class="num">1 <= class="num">0; class="kw">return temp; } ... } } class="kw">template<class="kw">typename T> T *applyProperties(T *ptr, class="kw">const Properties &props) { class="kw">static class="kw">const class="type">class="kw">string sides[class="num">4] = {"left", "top", "right", "bottom"}; for(class="type">int i = class="num">0; i < class="num">4; i++) { ptr[sides[i]] <= (class="type">int)props.margins[i]; } if(StringLen(props.text)) { ptr <= props.text; } else { ptr <= props.name; } ... class="kw">return ptr; } class="type">void inject(Properties &props) { CWnd *ptr = cache.get(props.name); if(ptr != NULL) { CWnd *sel = cache.getSelected(); if(ptr == sel) { update(ptr, props); Rebound(Rect()); } } ... } class="type">void update(CWnd *ptr, class="kw">const Properties &props) { ptr.Width(props.width); ptr.Height(props.height); ptr.Alignment(convert(props.align)); ptr.Margins(props.margins[class="num">0], props.margins[class="num">1], props.margins[class="num">2], props.margins[class="num">3]); CWndObj *obj = class="kw">dynamic_cast<CWndObj *>(ptr); if(obj) {
◍ 用事件映射把选中控件同步进检查器
这段逻辑解决的是 GUI 设计器里「点哪个、右侧就显示哪个」的联动问题。核心在 DefaultStdLayoutCache::onEvent:当控件被点击,先取消旧高亮(CONTROLS_BUTTON_COLOR_BORDER),再沿父链找真实元素,最后用 clrRed 高亮并通过 EventChartCustom 发 ON_LAYOUT_SELECTION 自定义事件。 InspectorDialog::OnRemoteSelection 接到事件后,从 designer.getCache() 取远端选中指针。StringSubstr(ptr.Name(),5) 切掉实例 ID 前缀,把纯名写回属性框;GetTypeByRTTI 按 _rtti 匹配下拉类型,再用 ptr.Width() / ptr.Height() 回填 SpinEdit 的数值。 代码里的 dynamic_cast 分支值得注意:CBoxH 走 HorizontalAlign,CBoxV 走 VerticalAlign,CEdit 走 TextAlign,三者都调 Pack() 或 return,说明布局重排只在复合框上触发。外汇与贵金属 MT5 面板开发属高风险定制,参数错乱可能导致界面卡死,建议先在模拟账户终端验证。 EditProperty::OnSetText 覆盖了文本提交:CEdit::OnSetText() 成功才写 property,避免空串污染。整体看,事件映射 + 缓存指针的方式比全局轮询更省资源,开 MT5 把这段贴进 EA 的 dialog 模块就能跑通选中联动。
obj.Text(props.text); } CBoxH *boxh = class="kw">dynamic_cast<CBoxH *>(ptr); if(boxh) { boxh.HorizontalAlign((HORIZONTAL_ALIGN)props.style); boxh.Pack(); class="kw">return; } CBoxV *boxv = class="kw">dynamic_cast<CBoxV *>(ptr); if(boxv) { boxv.VerticalAlign((VERTICAL_ALIGN)props.style); boxv.Pack(); class="kw">return; } CEdit *edit = class="kw">dynamic_cast<CEdit *>(ptr); if(edit) { edit.TextAlign(LayoutConverters::style2textAlign(props.style)); class="kw">return; } } EVENT_MAP_BEGIN(DesignerForm) ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) ... EVENT_MAP_END(AppDialogResizable) class DefaultStdLayoutCache: class="kw">public StdLayoutCache { class="kw">protected: CWnd *selected; class="kw">public: CWnd *getSelected(class="type">void) class="kw">const { class="kw">return selected; } ... class="kw">virtual class="type">bool onEvent(class="kw">const class="type">int event, CWnd *control) class="kw">override { if(control != NULL) { highlight(selected, CONTROLS_BUTTON_COLOR_BORDER); CWnd *element = control; if(!find(element)) class=class="str">"cmt">// this is an auxiliary object, not a compound control { element = findParent(element); class=class="str">"cmt">// get actual GUI element } ... selected = element; class="kw">const class="type">bool b = highlight(selected, clrRed); Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", element.Id()); EventChartCustom(CONTROLS_SELF_MESSAGE, ON_LAYOUT_SELECTION, class="num">0, class="num">0.0, NULL); class="kw">return true; } class="kw">return class="kw">false; } }; EVENT_MAP_BEGIN(InspectorDialog) ... ON_NO_ID_EVENT(ON_LAYOUT_SELECTION, OnRemoteSelection) EVENT_MAP_END(AppDialogResizable) class="type">bool InspectorDialog::OnRemoteSelection() { DefaultStdLayoutCache *remote = designer.getCache(); CWnd *ptr = remote.getSelected(); if(ptr) { class="type">class="kw">string purename = StringSubstr(ptr.Name(), class="num">5); class=class="str">"cmt">// cut instance id prefix CWndObj *x = class="kw">dynamic_cast<CWndObj *>(props.name.backlink()); if(x) x.Text(purename); props.name = purename; class="type">int t = -class="num">1; ComboBoxResizable *types = class="kw">dynamic_cast<ComboBoxResizable *>(props.type.backlink()); if(types) { t = GetTypeByRTTI(ptr._rtti); types.Select(t); props.type = t; } class=class="str">"cmt">// width and height SpinEditResizable *w = class="kw">dynamic_cast<SpinEditResizable *>(props.width.backlink()); w.Value(ptr.Width()); props.width = ptr.Width(); SpinEditResizable *h = class="kw">dynamic_cast<SpinEditResizable *>(props.height.backlink()); h.Value(ptr.Height()); props.height = ptr.Height(); ... } } class EditProperty: class="kw">public NotifiableProperty<CEdit,class="type">class="kw">string> { class="kw">public: ... class="kw">virtual class="type">bool OnSetText(class="type">void) class="kw">override { if(CEdit::OnSetText()) { if(CheckPointer(class="kw">property) != POINTER_INVALID) class="kw">property = m_text; class="kw">return true; } class="kw">return class="kw">false; } };