GUI:利用 MQL 创建您自己的图形库的提示和技巧·综合运用
🧩

GUI:利用 MQL 创建您自己的图形库的提示和技巧·综合运用

(3/3)· 当现成 MQL5 图形库太慢或难扩展,自己拼一套轻量结构其实没你想得重

进阶 第 3/3 篇

很多交易者装了庞大的第三方 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 先验证节奏。

MQL5 / C++
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(),避免新对象在图表上闪一下。下面这段是级联核心,直接贴出来逐行看:

MQL5 / C++
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 线主体上。真要嵌面板,建议把坐标改成基于图表右侧留白计算。

MQL5 / C++
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 修改生效。绕开办法是维护对象队列,把显示和层级变更标记后统一延迟执行,不常触发但写库时最好避开这种同时操作。

MQL5 / C++
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 里的例程加载出来,改一个对象参数看事件响应,比读十篇概述都实在。

把控件状态巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象树是否泄漏或层级错乱,你只管决定哪些面板真正该常驻。

常见问题

局部坐标是相对父元素的位置,全局坐标需递归累加父级偏移得到屏幕实际位置,事件命中测试通常依赖全局坐标。
可以,小布盯盘的 AIGC 模块能扫描图表对象树存活状态,提示未析构的子元素,减少手动排查成本。
概率上多是命中测试未同步新层级,或顶层透明元素拦截了消息,重排后需刷新事件分发优先级。
倾向保留对象切换可见性,避免频繁创建销毁带来名称和指针管理负担,但长期隐藏且占资源的可考虑销毁。