GUI:利用 MQL 创建您自己的图形库的提示和技巧·综合运用
(3/3)· 当现成 MQL5 图形库太慢或难扩展,自己拼一套轻量结构其实没你想得重
很多交易者装了庞大的第三方 GUI 库,却在想加一个特殊控件时被卡住:要么作者没空扩展,要么代码结构根本塞不进新逻辑。与其在别人的框架里硬改,不如弄懂对象层次和 Z 序这类底层机制,自己拼出刚好够用的那部分。
◍ 用时间帧属性做 GUI 对象的显隐
GUI 库要支持窗口开关、导航页切换或条件禁用按钮,都得有对象隐藏/显示能力。最笨的做法是每次删了重建,或者干脆不做;其实 MT5 对象有个原生属性 OBJPROP_TIMEFRAMES 能拿来顶替这功能——它本意是控制对象在哪些周期显示,但把它设成 OBJ_ALL_PERIODS 就全显示,设成 OBJ_NO_PERIODS 就全隐藏。 代码层用 ObjectSetInteger 改这个属性即可,但光靠它不够:必须区分“对象自己藏了”和“被父对象藏了”。否则递归调用时会出怪象——父藏了子还露着,或者按钮悬空没窗体。 下面这段是核心类的私有成员与对外接口。m_hidden 记自身标志,m_hidden_parent 记父级牵连;外部只调 Show()/Hide(),内部再递归处理子对象,并加保护避免重复调用。 Show() 里先判 IsHidden(),已显就 return;清掉 m_hidden 后调 ShowObject(),若程序指针有效则 RequestRedraw() 重绘。Hide() 则先置 m_hidden=true,若父已藏直接 return,否则 HideObject() 并视情况重绘。创建时就要按隐藏标记初始化,因为对象可能在建之前就被标了藏。 实盘接这套逻辑时记住:外汇/贵金属图表高频重绘,频繁显隐可能拖慢终端,建议在 Demo EA 里用数字键 0–9 绑 ID 先验证节奏。
ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); class=class="str">"cmt">//Show ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); class=class="str">"cmt">//Hide class CElement { class="kw">private: class=class="str">"cmt">//... class="type">bool m_hidden; class="type">bool m_hidden_parent; class="type">void HideObject(); class="type">void HideByParent(); class="type">void HideChildren(); class="type">void ShowObject(); class="type">void ShowByParent(); class="type">void ShowChildren(); class=class="str">"cmt">//... class="kw">public: class=class="str">"cmt">//... class="type">void Hide(); class="type">void Show(); class="type">bool IsHidden() { class="kw">return m_hidden || m_hidden_parent; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display element(if parent is also visible) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::Show(class="type">void) { if (!IsHidden()) class="kw">return; m_hidden = false; ShowObject(); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide element | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElement::Hide(class="type">void) { m_hidden = true; if (m_hidden_parent) class="kw">return; HideObject(); if (CheckPointer(m_program) != POINTER_INVALID)
对象显隐的父子级联逻辑
GUI 元素显隐不能只改自己,得顺着父子链走。CElement 里用 m_hidden_parent 标记父级是否藏起来了,自身 m_hidden 管自己的开关,两者独立。 ShowObject() 先判 IsHidden()(自身或父级任一隐藏就直接 return),否则把 OBJPROP_TIMEFRAMES 设成 OBJ_ALL_PERIODS 并递归 ShowChildren()。HideObject() 则把时间帧设成 OBJ_NO_PERIODS 再往下藏孩子。 级联的关键在 ShowByParent / HideByParent:父级显示时清掉 m_hidden_parent 再调 ShowObject();父级隐藏时置 m_hidden_parent=true,仅当自身 m_hidden 为假才真正 HideObject()。这样子对象能记住「爹藏了但我自己没藏」的状态。 CreateChildren() 建完子树后若 IsHidden() 为真立即 HideObject(),避免新对象在图表上闪一下。下面这段是级联核心,直接贴出来逐行看:
class="type">void CElement::ShowObject(class="type">void) { if(IsHidden()) class=class="str">"cmt">//Parent or self class="kw">return; ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); ShowChildren(); } class="type">void CElement::ShowByParent(class="type">void) { m_hidden_parent=false; ShowObject(); } class="type">void CElement::ShowChildren(class="type">void) { for(class="type">int i=class="num">0; i<m_child_count; i++) m_children[i].ShowByParent(); } class="type">void CElement::HideObject(class="type">void) { ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); HideChildren(); } class="type">void CElement::HideByParent(class="type">void) { m_hidden_parent=true; if(m_hidden) class="kw">return; HideObject(); } class="type">void CElement::HideChildren(class="type">void) { for(class="type">int i=class="num">0; i<m_child_count; i++) m_children[i].HideByParent(); } class="type">void CElement::CreateChildren(class="type">void) { for(class="type">int i=class="num">0; i<m_child_count; i++) { m_children[i].Create(); m_children[i].CreateChildren(); } if(IsHidden()) HideObject(); }
「用数字键控制图形隐藏的底层写法」
在 MT5 自定义 GUI 里,想让某个拖拽元素按键盘数字键切换显隐,核心是把按键事件和元素 ID 绑定。下面这段从 CHideElement 类看逻辑:构造函数只干一件事,把传入的 int 型 id 存进 key_id 成员,比如 new CHideElement(3) 就代表按数字键『3』控制它。 OnEvent 里先调父类的 CDragElement::OnEvent 保证拖拽照常,再判 id != CHARTEVENT_KEYDOWN 就直接返回,不抢其他事件。真正切换发生在 lparam == '0' + key_id 时——字符『0』的 ASCII 是 48,加 key_id 正好对应主键盘数字行,命中就 IsHidden() 判断后 Show() 或 Hide()。 DrawCanvas 每次重绘用 MathRand()%256 填随机 RGB 背景,Arial 50 号字在 (25,25) 坐标写 white 色的 key_id,方便肉眼确认哪个方块归哪个键。 OnInit 里 MathSrand(TimeLocal()) 播种子,max_x/max_y 各减 100 是留默认元素尺寸;循环 0~9 建 10 个 CHideElement 随机撒在图面。外汇与贵金属图表挂这类脚本属高风险实验,按键逻辑出错可能遮住价格区域,上真盘前请在模拟图表跑通。 别把随机坐标当布局方案 MathRand()%max_x 只是demo占位,实盘若用同样写法,元素可能压在 K 线主体上。真要嵌面板,建议把坐标改成基于图表右侧留白计算。
class="kw">virtual class="type">bool OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam); class="kw">public: CHideElement(class="type">int id); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide element constructor(set keyboard ID) | class=class="str">"cmt">//+------------------------------------------------------------------+ CHideElement::CHideElement(class="type">int id) : key_id(id) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Hide element when its key is pressed(inherit drag events) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CHideElement::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class="type">bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) class="kw">return drag; if (lparam == &class="macro">#x27;class="num">0&class="macro">#x27; + key_id) class=class="str">"cmt">//Toggle hide/show { if (IsHidden()) Show(); else Hide(); } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw canvas function(fill with class="type">class="kw">color and display number ID) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CHideElement::DrawCanvas(class="type">void) { m_canvas.Erase(ARGB(class="num">255, MathRand() % class="num">256, MathRand() % class="num">256, MathRand() % class="num">256)); m_canvas.FontSet("Arial", class="num">50); m_canvas.TextOut(class="num">25, class="num">25, IntegerToString(key_id), ColorToARGB(clrWhite)); m_canvas.Update(false); } class="type">int OnInit() { MathSrand((class="type">uint)TimeLocal()); class=class="str">"cmt">//class="num">100 is element size by class="kw">default class="type">int max_x = (class="type">int)ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS) - class="num">100; class="type">int max_y = (class="type">int)ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS) - class="num">100; for (class="type">int i = class="num">0; i < class="num">10; i++) { CHideElement* drawing = new CHideElement(i); drawing.SetPosition(MathRand() % max_x, MathRand() % max_y); program.AddMainElement(drawing); } program.CreateGUI(); ChartRedraw(class="num">0); class="kw">return(INIT_SUCCEEDED); }
◍ 用隐藏再显示偷换MT5对象层级
MT5 里对象没有公开 API 让你直接改 Z 序,但渲染规则很死:后创建或刚被刷新属性的对象会压在顶层。实测发现,对一个已存在的图形对象执行一次隐藏再显示,它就会重新冒到所有对象之上——这不是 bug,是终端重绘顺序决定的。 上面那段派生类代码就是利用这点来‘提升层级’:每次按下绑定按键,先把它 OBJPROP_TIMEFRAMES 设成 OBJ_NO_PERIODS,再设回 OBJ_ALL_PERIODS,等效于隐藏后立刻显示,对象便置顶。末尾调用 RequestRedraw 是必须的,否则画面不会刷新。 [CODE] bool CRaiseElement::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) return drag; if (lparam == '0' + key_id) { ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } return true; } [/CODE] 逐行拆解:第1行定义事件处理函数入口,接收图表事件四参数。第3行先交给父类 CDragElement 处理拖拽逻辑并取返回值。第4–5行过滤非按键事件直接返回。第6–7行判断按下的键码是否匹配本对象绑定值。第9行把对象周期属性设为无周期(相当于隐藏)。第10行恢复为全部周期(相当于显示并置顶)。第11–12行确认程序指针有效后请求重绘。第14行返回 true 吃掉事件。 想只升一级而非置顶,就按从下到上顺序对目标及其上方对象依次做这套‘无周期→全周期’操作;想沉到底,则对全部对象重排一遍。外汇与贵金属图表叠加对象多,误置顶可能挡掉关键 K 线,实操前建议在模拟图表验证。 有个坑得留心:同一刻既不能显示被藏对象又改它 Z 序。例如先 show 按钮(设全周期)再提升窗口(无周期→全周期),按钮会卡在窗口后——似乎只有首次 TIMEFRAMES 修改生效。绕开办法是维护对象队列,把显示和层级变更标记后统一延迟执行,不常触发但写库时最好避开这种同时操作。
class="type">bool CRaiseElement::OnEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class="type">bool drag = CDragElement::OnEvent(id, lparam, dparam, sparam); if (id != CHARTEVENT_KEYDOWN) class="kw">return drag; if (lparam == &class="macro">#x27;class="num">0&class="macro">#x27; + key_id) { ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); ObjectSetInteger(class="num">0, m_name, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); if (CheckPointer(m_program) != POINTER_INVALID) m_program.RequestRedraw(); } class="kw">return true; }
别急着下结论
这套 GUI 库的演示代码只实现了创建和组合对象的最低限度功能,下载包 GUIArticle.zip 体积 91.13 KB,能跑通文内每个小示例,但离全功能还差得远。 一旦你开始往里加更多控件类型,尤其是彼此带事件联动的子对象,复杂度会陡增——别以为现在几十行就能一直这么轻。 其它库的架构可能因设计取舍或性能需求而完全不同,本文只是给你一个可验证的起点:开 MT5 把 ZIP 里的例程加载出来,改一个对象参数看事件响应,比读十篇概述都实在。