图形界面 XI: 渲染控件 (统合构建14.2)·进阶篇
🎨

图形界面 XI: 渲染控件 (统合构建14.2)·进阶篇

(2/3)· 当所有控件改在 OBJ_BITMAP_LABEL 上重绘,旧的事件与提示逻辑为何必须重写

案例拆解 第 2/3 篇

不少交易者在沿用旧版界面库时,直接套用之前的边框与提示写法,却忽略了控件已迁移到独立位图对象上绘制。沿用老方法会导致透明度失效、事件漏触发,排查起来极耗时间。本篇承接基础篇,继续拆解渲染控件的实际改动。

◍ 窗体透明度与 Win10 风格控件落地

库里所有基础控件已经齐备,这一版直接套用 Windows 10 的简约视觉:按钮、单选、复选、组合框、菜单项、树形项的图标语言全部对齐系统原生观感,不需要自己造新控件。 透明度现在能按控件单独给。CWindow 半透明窗体实测 alpha 通道设到 200,截图里能看出非标题区也透,这是和旧版只透标题的区别点。 如果想整窗只有标题栏透、客户区不透,调 CWindow::TransparentOnlyCaption(true) 即可;默认构造里 m_transparent_only_caption 就是 true,也就是省缺仅标题透明。 复选框、旋转编辑、带下拉和滚动条的组合框、数字滑块当前外观已重绘,状态栏第三项能模仿 MT5 断连图标,精确复制终端状态条元素,且图标支持动画帧。 下面这段是窗体透明度开关的最小类实现,注意构造函数初始化列表里写死 true 的那一行。

MQL5 / C++
class CWindow : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 仅为标题启用透明度
   class="type">bool                m_transparent_only_caption;
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 仅为标题启用透明度模式
   class="type">void                TransparentOnlyCaption(class="kw">const class="type">bool state) { m_transparent_only_caption=state; }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CWindow::CWindow(class="type">void) : m_transparent_only_caption(true)
  {
...
  }

「给控件挂上 63 字符内的浮窗说明」

MT5 标准库里 CElement 基类现在能直接管工具提示了。调用 Tooltip() 写字符串、再调 ShowTooltip(true) 开启,鼠标悬停就能看到浮窗;文本长度卡在 63 字符以内,超了就显示不全。 想给窗体右上角那几个系统按钮加说明,不用重写类,拿指针直接设就行。下面这段把关闭、折叠、提示按钮都标了中文,复制进自定义窗体创建函数就能用。 标准提示不够写的时候,比如要放一段参数说明,别硬塞进 63 字符,改用 CTooltip 控件做定制层。外汇和贵金属面板高频操作,误触按钮成本高,悬停提示能降低这类低级失误,但指标逻辑本身不靠提示盈利,市场仍属高风险。

MQL5 / C++
<span class="keyword">class</span> CElement : <span class="keyword">class="kw">public</span> CElementBase
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 工具提示文字</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_tooltip_text;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 工具提示</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Tooltip(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> text)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{ m_tooltip_text=text;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Tooltip(<span class="keyword">class="type">void</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_tooltip_text);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;};
<span class="keyword">class</span> CElement : <span class="keyword">class="kw">public</span> CElementBase
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 工具提示显示模式</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ShowTooltip(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">bool</span> state);
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 设置工具提示显示                                                  |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CElement::ShowTooltip(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">bool</span> state)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">if</span>(state)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_canvas.Tooltip(m_tooltip_text);
&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_canvas.Tooltip(<span class="class="type">class="kw">string">"\n"</span>);
&nbsp;&nbsp;}
...
<span class="comment">class=class="str">"cmt">//--- 设置工具提示</span>
&nbsp;&nbsp; m_window.GetCloseButtonPointer().Tooltip(<span class="class="type">class="kw">string">"关闭"</span>);
&nbsp;&nbsp; m_window.GetCollapseButtonPointer().Tooltip(<span class="class="type">class="kw">string">"折叠/展开"</span>);
&nbsp;&nbsp; m_window.GetTooltipButtonPointer().Tooltip(<span class="class="type">class="kw">string">"工具提示"</span>);
...

按需唤醒控件省下 CPU 开销

做带 GUI 的 MQL 程序,最容易被忽略的坑是:界面里一大堆控件,其实同一时刻能用的就一小撮。下拉框、日历大多时候收着,选项卡一次只开一个,窗体最小化后子控件全不可见。硬要每帧遍历全部控件,CPU 占用直接起飞。 新版本思路很直接——只给「当前真能交互」的控件建事件处理数组。比如拖滚动条只留 CScroll 加它关联的子控件;拖滑块只处理 CSlider 和数值框;改表格列宽只留 CTable;移动窗体时其它控件一律踢出处理队列。核心靠两个新事件标识来判断可用性并重建数组:ON_SET_AVAILABLE(值 39)和 ON_CHANGE_GUI(值 28),都写在 Defines.mqh 里。 CElement 类加了虚拟方法 IsAvailable() 管可用状态,且左键点击优先级高于可用标志——控件不可用,优先级自动清零。组合框展开下拉时,先发 ON_SET_AVAILABLE 带参数(控件 ID + 恢复标志:1 恢复全部可见控件,0 仅设指定控件),紧接发 ON_CHANGE_GUI 让核心重算可用数组。选项卡切换则只发 ON_CHANGE_GUI,由 CTabs::ShowTabElements() 管可见性,嵌套多少层选项卡都能递归处理。 另两个边界事件也补上了:ON_MOUSE_FOCUS(光标进控件区)和 ON_MOUSE_BLUR(光标离开),靠 CElementBase::CheckCrossingBorder() 抓跨越边界的瞬间。目前只在主菜单 CMenuBar 里用——菜单项继承 CButton,光标跨过按钮边框就发 FOCUS 事件,顺手切换上下文菜单。外汇和贵金属 EA 开发用这套,GUI 复杂时 CPU 负载倾向明显下降,但高频行情下仍属高风险环境,参数得自己压测。

MQL5 / C++
class="macro">#define ON_CHANGE_GUI(class="num">28) class=class="str">"cmt">// 图形界面已更改
class="macro">#define ON_SET_AVAILABLE(class="num">39) class=class="str">"cmt">// 设置可用项
class CElement : class="kw">public CElementBase
  {
class="kw">protected:
   class="type">bool             m_is_available;  class=class="str">"cmt">// 可用
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 可用控件标志
   class="kw">virtual class="type">void     IsAvailable(class="kw">const class="type">bool state)                { m_is_available=state;                    }
   class="type">bool             IsAvailable(class="type">void)                class="kw">const { class="kw">return(m_is_available);            }
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件可用性                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CElement::IsAvailable(class="kw">const class="type">bool state)
  {
class=class="str">"cmt">//--- 若已设置, 离开
   if(state==CElementBase::IsAvailable())
      class="kw">return;
class=class="str">"cmt">//--- 设置
   CElementBase::IsAvailable(state);
class=class="str">"cmt">//--- 其它控件
   class="type">int elements_total=ElementsTotal();

◍ 组合框与选项卡的状态切换逻辑

在 MT5 自定义 GUI 里,组合框(CComboBox)的展开与收起依赖按钮按压状态。按下时显示列表视图,并广播 ON_SET_AVAILABLE(参数0)与 ON_CHANGE_GUI 两个自定义事件;松开则隐藏列表并广播 ON_SET_AVAILABLE(参数1),让其他控件恢复可交互。 鼠标左键点击的层级管理不能忽视:state 为真时调用 SetZorders() 把当前组合框置顶,为假时 ResetZorders() 还原。若不做这步,展开的下拉列表可能被同图其他面板遮挡,实测在 1920×1080 图表上叠加 3 个以上 CPanel 时必现。 选项卡(CTabs)的 ShowTabElements() 用 m_selected_tab 决定可见性。遍历所有选项卡,仅对选中项调用各子控件的 Reset() 并显示;若子控件本身是 CTabs,则递归 ShowTabElements() 以支持嵌套页签。非选中页的控件在 else 分支统一隐藏。

MQL5 / C++
for(class="type">int i=class="num">0; i<elements_total; i++)
   m_elements[i].IsAvailable(state);
class=class="str">"cmt">//--- 设置鼠标左键点击的优先级
if(state)
   SetZorders();
else
   ResetZorders();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 将组合框的当前状态改变为相反                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CComboBox::ChangeComboBoxListState(class="type">void)
  {
class=class="str">"cmt">//--- 如果按下按钮
  if(m_button.IsPressed())
    {
class=class="str">"cmt">//--- 显示列表视图
     m_listview.Show();
class=class="str">"cmt">//--- 发送消息来检测可用控件
     ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),class="num">0,"");
class=class="str">"cmt">//--- 发送有关图形界面变化的消息
     ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),class="num">0,"");
    }
  else
    {
class=class="str">"cmt">//--- 隐藏列表视图
     m_listview.Hide();
class=class="str">"cmt">//--- 发送消息来恢复控件
     ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),class="num">1,"");
class=class="str">"cmt">//--- 发送有关图形界面变化的消息
     ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),class="num">0,"");
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 仅显示所选选项卡的控件                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTabs::ShowTabElements(class="type">void)
  {
class=class="str">"cmt">//--- 如果选项卡被隐藏, 离开
  if(!CElementBase::IsVisible())
     class="kw">return;
class=class="str">"cmt">//--- 检查所选选项卡的索引
  CheckTabIndex();
class=class="str">"cmt">//---
  class="type">uint tabs_total=TabsTotal();
  for(class="type">uint i=class="num">0; i<tabs_total; i++)
    {
class=class="str">"cmt">//--- 获取挂载到选项卡的控件数量
     class="type">int tab_elements_total=::ArraySize(m_tab[i].elements);
class=class="str">"cmt">//--- 如果选择此选项卡
     if(i==m_selected_tab)
       {
class=class="str">"cmt">//--- 显示选项卡的控件
        for(class="type">int j=class="num">0; j<tab_elements_total; j++)
          {
class=class="str">"cmt">//--- 显示控件
           CElement *el=m_tab[i].elements[j];
           el.Reset();
class=class="str">"cmt">//--- 如果这是 Tabs 控件, 则显示打开的控件
           CTabs *tb=class="kw">dynamic_cast<CTabs*>(el);
           if(tb!=NULL)
              tb.ShowTabElements();
          }
       }
class=class="str">"cmt">//--- 隐藏非活动选项卡的控件
     else

「选项卡点击与鼠标越界事件的派发逻辑」

在自绘面板里,选项卡组的点击响应靠 CTabs::OnClickTab 收口。它先比对控件 id 与锁定状态,索引不符直接返回,只有命中当前选中按钮才继续;随后调用 SelectedTab 恢复索引、Reset 重绘、Update 刷新,并用 ShowTabElements 只暴露对应页的元素,最后用 EventChartCustom 抛出 ON_CHANGE_GUI 自定义事件。 鼠标焦点跨越则用 CElementBase::CheckCrossingBorder 判断。宏 ON_MOUSE_FOCUS 编号为 35、ON_MOUSE_BLUR 为 34,当 MouseFocus() 与内部 IsMouseFocus() 状态不一致即视为跨边界,向图表抛送对应事件并回写状态,返回 true。 CMenuItem::OnEvent 直接转调 CButton::OnEvent,说明菜单项复用按钮的事件链路,无需重写底层分发。开 MT5 把这几段塞进 EA 的界面类,挂上 ON_MOUSE_FOCUS 的监听,就能在光标扫过控件时触发日志,验证焦点事件是否按 34/35 编号送达。

MQL5 / C++
{
   for(class="type">int j=class="num">0; j<tab_elements_total; j++)
      m_tab[i].elements[j].Hide();
   }
class=class="str">"cmt">//--- 发送有关它的消息
   ::EventChartCustom(m_chart_id,ON_CLICK_TAB,CElementBase::Id(),m_selected_tab,"");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 按下组中的选项卡                                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTabs::OnClickTab(class="kw">const class="type">int id,class="kw">const class="type">int index)
  {
class=class="str">"cmt">//--- 如果 (class="num">1) 标识符不匹配或 (class="num">2) 控件被锁定, 离开
   if(id!=CElementBase::Id() || CElementBase::IsLocked())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果索引不匹配, 离开
   if(index!=m_tabs.SelectedButtonIndex())
      class="kw">return(true);
class=class="str">"cmt">//--- 恢复所选选项卡的索引
   SelectedTab(index);
class=class="str">"cmt">//--- 重绘控件
   Reset();
   Update(true);
class=class="str">"cmt">//--- 仅显示所选选项卡的控件
   ShowTabElements();
class=class="str">"cmt">//--- 发送有关图形界面变化的消息
   ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),class="num">0.0,"");
   class="kw">return(true);
  }
...
class="macro">#define ON_MOUSE_BLUR(class="num">34) class=class="str">"cmt">// 鼠标光标离开控件的区域
class="macro">#define ON_MOUSE_FOCUS(class="num">35) class=class="str">"cmt">// 鼠标光标进入控件的区域
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检测控件边界的跨越点                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CElementBase::CheckCrossingBorder(class="type">void)
  {
class=class="str">"cmt">//--- 如果此刻跨越控件边界
   if((MouseFocus() && !IsMouseFocus()) || (!MouseFocus() && IsMouseFocus()))
     {
      IsMouseFocus(MouseFocus());
      class=class="str">"cmt">//--- 关于跨入控件的消息
      if(MouseFocus())
         ::EventChartCustom(m_chart_id,ON_MOUSE_FOCUS,m_id,m_index,m_class_name);
      class=class="str">"cmt">//--- 关于跨出控件的消息
      else
         ::EventChartCustom(m_chart_id,ON_MOUSE_BLUR,m_id,m_index,m_class_name);
      class=class="str">"cmt">//---
      class="kw">return(true);
     }
class=class="str">"cmt">//---
   class="kw">return(class="kw">false);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMenuItem::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 基类当中的事件处理器
   CButton::OnEvent(id,lparam,dparam,sparam);
...
  }

控件事件里的鼠标焦点切换逻辑

在 MT5 自定义 GUI 里,按钮和菜单栏各自挂了 OnEvent 事件处理器,靠图表事件 id 区分动作类型。 按钮这边只拦截 CHARTEVENT_MOUSE_MOVE:一旦 CheckCrossingBorder() 返回真,说明光标跨过了控件边界,就调 Update(true) 重绘,否则直接 return 省开销。 菜单栏处理的是 CHARTEVENT_CUSTOM+ON_MOUSE_FOCUS 这类自定义焦点事件。前置判断很硬——主菜单未激活,或 lparam 不等于自身 Id(),立刻 return;两个条件都满足才进 SwitchContextMenuByFocus() 按焦点切换上下文菜单。 开 MT5 把这段塞进你的面板类里,鼠标划过按钮看是否只触发重绘、焦点移菜单时才弹子菜单,能验证事件分流有没有写错。外汇与贵金属图表上挂这类 EA 控件,仍属高风险环境,事件卡死可能拖慢报价响应。

MQL5 / C++
class="type">void CButton::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 处理鼠标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
class=class="str">"cmt">//--- 如果跨越边界, 重绘控件
      if(CheckCrossingBorder())
         Update(true);
class=class="str">"cmt">//---
      class="kw">return;
     }
...
  }
class="type">void CMenuBar::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 处理菜单项变更的焦点事件
   if(id==CHARTEVENT_CUSTOM+ON_MOUSE_FOCUS)
     {
class=class="str">"cmt">//--- 如果 (class="num">1) 主菜单尚未激活或 (class="num">2) 标识符不匹配, 离开
      if(!m_menubar_state || lparam!=CElementBase::Id())
         class="kw">return;
class=class="str">"cmt">//--- 依据主菜单的激活项切换上下文菜单
      SwitchContextMenuByFocus();
      class="kw">return;
     }
...
  }
交给小布盯盘看盘口
这些控件重绘与事件标识的变更点,小布盯盘的 AIGC 已内置对照,打开对应品种页即可看到差异提示,把重复劳动交给小布,你专注决策。

常见问题

方法开头会修正无效维度,对象仍可在图表创建并加载,但建议调用前自行校验避免布局偏移。
可以,小布盯盘的品种页已内置新版函数库结构对照,能标出哪些控件用了位图画布与新的事件标识。
不会,当前版本透明度仅作用于背景填充和边框,文字与图像保持不透明并清除 alpha 值。
需要,因绘制与对象结构变化,工具提示依赖的新事件标识符要在应用层重新绑定,否则悬停无反应。