图形界面 X: 简单快速开发库的更新 (版本 2)·进阶篇
📘

图形界面 X: 简单快速开发库的更新 (版本 2)·进阶篇

第 2/3 篇

「把鼠标坐标喂给界面控件」

在 MT5 自定义面板里,鼠标位置必须实时转成时间/价格坐标,否则画出来的线会漂。CMouse::OnEvent 只认 CHARTEVENT_MOUSE_MOVE,左键状态从 sparam 强转 bool,x/y 分别从 lparam、dparam 取整。 若 ChartXYToTimePrice(0,m_x,m_y,m_subwin,m_time,m_level) 返回 false 直接 return,说明光标不在主图有效区;子窗口(m_subwin>0)还要减去 SubwindowY 偏移,不然 Y 轴相对坐标算错。 CWndContainer 持有 CMouse 实例,CElement 基类通过 MousePointer(CMouse &object) 把指针存进去,GetPointer 返回的是同一对象引用而非拷贝——这对高频鼠标事件很关键,避免每帧new对象。 AddToObjectsArray 模板函数在把控件塞进数组后,会调用 object.MousePointer(m_mouse) 让每个控件共享容器里的鼠标实例。你在 EA 里接这套,开 MT5 把这段贴进类定义,拖一下面板就能验证光标跟随是否准。

MQL5 / C++
class="type">void CMouse::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=class="str">"cmt">//--- 光标移动事件处理函数
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
class=class="str">"cmt">//--- 鼠标左键的坐标和状态
       m_x                =(class="type">int)lparam;
       m_y                =(class="type">int)dparam;
       m_left_button_state =(class="type">bool)class="type">int(sparam);
class=class="str">"cmt">//--- 取得光标位置
       if(!::ChartXYToTimePrice(class="num">0,m_x,m_y,m_subwin,m_time,m_level))
         class="kw">return;
class=class="str">"cmt">//--- 取得相对 Y 坐标
       if(m_subwin>class="num">0)
         m_y=m_y-m_chart.SubwindowY(m_subwin);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于保存所有界面对象的类                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWndContainer
  {
class="kw">protected:
  class=class="str">"cmt">//--- 用于取得鼠标参数的类的实例
   CMouse            m_mouse;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件的基类                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CElement
  {
class="kw">protected:
  class=class="str">"cmt">//--- 用于取得鼠标参数的类的实例
   CMouse           *m_mouse;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- (class="num">1) 保存 和 (class="num">2) 返回 鼠标的指针
   class="type">void             MousePointer(CMouse &object)                  { m_mouse=::GetPointer(object);     }
   CMouse          *MousePointer(class="type">void)                  const { class="kw">return(::GetPointer(m_mouse));  }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 把控件对象的指针加到通用数组中                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">template<class="kw">typename T>
class="type">void CWndContainer::AddToObjectsArray(const class="type">int window_index,T &object)
  {
   class="type">int total=object.ObjectsElementTotal();
   for(class="type">int i=class="num">0; i<total; i++)
      AddToArray(window_index,object.Object(i));
class=class="str">"cmt">//--- 在控件基类中保存鼠标光标
   object.MousePointer(m_mouse);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 处理程序的事件                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CWndEvents::ChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {

◍ 按钮控件的鼠标事件拦截逻辑

在 MT5 自定义 GUI 里,按钮类 CSimpleButton 的 OnEvent 方法只认 CHARTEVENT_MOUSE_MOVE 这一种事件 id,其余图表事件直接略过。这样做能把鼠标响应收敛到最小开销,避免每帧都去遍历所有控件状态。 进入鼠标移动分支后,代码先做了四道闸门:控件不可见、所属表单被锁定、子窗口编号对不上、按钮自身被禁用,任一条件命中就 return。其中子窗口匹配判断用的是 CElement::m_mouse.SubWindowNumber() 与控件记录的 m_subwin 做不等比较,这意味着跨副图拖拽时按钮不会误触发。 焦点判定靠坐标夹逼:用鼠标 X/Y 分别和控件的 X()/X2()、Y()/Y2() 比大小,四个条件全真才置 MouseFocus 为真。紧接着若左键未按下(LeftButtonState 为假)也直接退出,所以纯悬停不会引发点击逻辑,只有「在按钮区域内 + 按住左键」才可能往下走。 基类 CElement 只暴露了 IdFromObjectName 和 IndexFromObjectName 两个 protected 方法,供派生类从图形对象名反查控件 ID 或菜单索引;CWindow 在此基础上私有化了抬头标签缩进 m_label_x_gap / m_label_y_gap 和折叠按钮开关 m_roll_button,表单外观参数被收进类内部而非暴露给外部调用。

MQL5 / C++
class="type">void CSimpleButton::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=class="str">"cmt">//--- 处理光标移动事件
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
class=class="str">"cmt">//--- 如果控件隐藏,就退出
      if(!CElement::IsVisible())
         class="kw">return;
class=class="str">"cmt">//--- 如果表单被屏蔽,退出
      if(m_wnd.IsLocked())
         class="kw">return;
class=class="str">"cmt">//--- 如果子窗口编号不匹配,退出
      if(CElement::m_subwin!=CElement::m_mouse.SubWindowNumber())
         class="kw">return;
class=class="str">"cmt">//--- 如果按钮被屏蔽,退出
      if(!m_button_state)
         class="kw">return;
class=class="str">"cmt">//--- 定义焦点
      CElement::MouseFocus(CElement::m_mouse.X()>X() && CElement::m_mouse.X()<X2() &&
                           CElement::m_mouse.Y()>Y() && CElement::m_mouse.Y()<Y2());
class=class="str">"cmt">//--- 如果鼠标按键松开,退出
      if(!CElement::m_mouse.LeftButtonState())
         class="kw">return;
class=class="str">"cmt">//--- 
      ...
      class="kw">return;
     }
class=class="str">"cmt">//...
  }

class CElement
  {
class="kw">protected:
  class=class="str">"cmt">//--- 根据按钮名称取得ID
   class="type">int               IdFromObjectName(const class="type">class="kw">string object_name);
  class=class="str">"cmt">//--- 根据菜单选项名称取得索引
   class="type">int               IndexFromObjectName(const class="type">class="kw">string object_name);
  };

class CWindow : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 抬头文字标签的缩进
   class="type">int               m_label_x_gap;
   class="type">int               m_label_y_gap;
  class=class="str">"cmt">//--- 是否有按钮来进行窗口的展开/折叠
   class="type">bool              m_roll_button;
  class=class="str">"cmt">//---
  };

控件的标签缩进与指针校验

在自定义 GUI 控件的公开接口里,LabelXGap() 和 LabelYGap() 直接把传入的整型参数写进 m_label_x_gap / m_label_y_gap 成员变量,用来控制文字标签相对锚点的像素缩进;UseRollButton() 则把 m_roll_button 置真,启用卷动按钮。这三个 setter 没有返回值,调用后需自行确认对象状态。 所有控件都继承自 CElement,它的保护方法 CheckWindowPointer() 接收一个 ENUM_POINTER_TYPE 枚举。若传入 POINTER_INVALID,函数会拼出含 __FUNCTION__ 和 ClassName() 的报错串,用 ::Print() 打到终端日志并返回 false,从而中断界面创建;指针有效时才返回 true。 CSimpleButton::CreateSimpleButton() 在开头就调 CElement::CheckWindowPointer(::CheckPointer(m_wnd)),若表单指针为空直接 return(false)。随后 m_id 取 m_wnd.LastId()+1,并把图表的 chart_id、subwin 及 x/y 坐标、button_text 逐一写入成员,再用 m_x-m_wnd.X() 和 m_y-m_wnd.Y() 算出行列间隙后调 CreateButton()。开 MT5 建面板时,若日志冒出“在创建控件之前,表单指针 … 是必需的”,就是这里拦的。

MQL5 / C++
class="kw">public:
  class=class="str">"cmt">//--- 文字标签的缩进
  class="type">void                LabelXGap(const class="type">int x_gap)                  { m_label_x_gap=x_gap;                }
  class="type">void                LabelYGap(const class="type">int y_gap)                  { m_label_y_gap=y_gap;                }
  class=class="str">"cmt">//--- 使用卷动按钮
  class="type">void                UseRollButton(class="type">void)                         { m_roll_button=true;                 }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件的基类                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CElement
  {
class="kw">protected:
  class=class="str">"cmt">//--- 检查表单指针是否存在
  class="type">bool                CheckWindowPointer(ENUM_POINTER_TYPE pointer_type);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查表单指针是否存在                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CElement::CheckWindowPointer(ENUM_POINTER_TYPE pointer_type)
  {
class=class="str">"cmt">//--- 如果没有表单指针
  if(pointer_type==POINTER_INVALID)
    {
      class=class="str">"cmt">//--- 生成消息
      class="type">class="kw">string message=__FUNCTION__+" > 在创建控件之前,表单指针: "+ClassName()+"::WindowPointer(CWindow &object) 是必需的";
      class=class="str">"cmt">//--- 发送消息到终端日志
      ::Print(message);
      class=class="str">"cmt">//--- 打断应用程序图形界面的创建
      class="kw">return(false);
    }
class=class="str">"cmt">//--- 发送指针存在的结果
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建"简单按钮"控件                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleButton::CreateSimpleButton(const class="type">long chart_id,const class="type">int subwin,const class="type">class="kw">string button_text,const class="type">int x,const class="type">int y)
  {
class=class="str">"cmt">//--- 如果没有表单指针,退出
  if(!CElement::CheckWindowPointer(::CheckPointer(m_wnd)))
     class="kw">return(false);
class=class="str">"cmt">//--- 初始化变量
  m_id        =m_wnd.LastId()+class="num">1;
  m_chart_id  =chart_id;
  m_subwin    =subwin;
  m_x         =x;
  m_y         =y;
  m_button_text=button_text;
class=class="str">"cmt">//--- 与边缘之间的距离
  CElement::XGap(m_x-m_wnd.X());
  CElement::YGap(m_y-m_wnd.Y());
class=class="str">"cmt">//--- 创建按钮
  if(!CreateButton())

「控件随图表宽度自适应的落地写法」

在自定义面板里,最烦的就是图表一缩放,控件还钉在老位置、老宽度,看起来像残了。CElement 基类给了一个开关:m_auto_xresize_mode,默认 false,构造函数里直接置位,不干预布局。 想让某个控件跟着图表右边走,先调 AutoXResizeMode(true),再用 AutoXResizeRightOffset(int) 设它右侧离表单边的距离(像素)。这两个都是 inline 存取,读和写分开重载,不会互相踩。 真正触发在 CHARTEVENT_CHART_CHANGE:若没按住拖拽(NOT_PRESSED),先 SetWindowProperties 取窗口尺寸、UpdateWindowXY 修正坐标;随后若 AutoXResizeMode() 为真,直接 ChangeWindowWidth(m_chart.WidthInPixels()-2)——减 2 是留边,避免贴死右框。外汇与贵金属图表多屏缩放频繁,这种自适应能省掉手动重绘,但高频 resize 下重算坐标有轻微开销,实盘前请在 MT5 用不同模板验证。

MQL5 / C++
class="kw">return(false);
class=class="str">"cmt">//--- 如果是对话框窗口或者是最小化的,隐藏控件
  if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 控件基类                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CElement
  {
class="kw">protected:
  class=class="str">"cmt">//--- 控件宽度自动改变模式
  class="type">bool                m_auto_xresize_mode;
  class=class="str">"cmt">//--- 自动改变控件宽度模式下,控件右侧与表单右边之间的距离
  class="type">int                 m_auto_xresize_right_offset;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- (class="num">1) 控件宽度自动改变模式, (class="num">2) 设置/取得与表单右边的距离
  class="type">bool                AutoXResizeMode(class="type">void)                    const { class="kw">return(m_auto_xresize_mode);       }
  class="type">void                AutoXResizeMode(const class="type">bool flag)                { m_auto_xresize_mode=flag;            }
  class="type">int                 AutoXResizeRightOffset(class="type">void)             const { class="kw">return(m_auto_xresize_right_offset); }
  class="type">void                AutoXResizeRightOffset(const class="type">int offset)        { m_auto_xresize_right_offset=offset;  }
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
CElement::CElement(class="type">void) : m_auto_xresize_mode(false),
                           m_auto_xresize_right_offset(class="num">0)
  {
  }
class=class="str">"cmt">//--- 图表属性改变事件
   if(id==CHARTEVENT_CHART_CHANGE)
    {
    class=class="str">"cmt">//--- 如果是松开按键
    if(m_clamping_area_mouse==NOT_PRESSED)
      {
      class=class="str">"cmt">//--- 取得图表窗口大小
      SetWindowProperties();
      class=class="str">"cmt">//--- 修正坐标
      UpdateWindowXY(m_x,m_y);
      }
    class=class="str">"cmt">//--- 如果启用了这种模式,改变大小
    if(CElement::AutoXResizeMode())
      ChangeWindowWidth(m_chart.WidthInPixels()-class="num">2);
    class=class="str">"cmt">//---
    class="kw">return;
    }

常见问题

在图表鼠标移动事件里取坐标值,写进控件的文本属性并触发重绘;注意只在该控件区域命中时才更新,避免全局刷屏卡顿。
多半是上层控件先截获了鼠标消息且未向下传递;检查按钮的命中矩形和事件返回逻辑,确保未占用的区域交还给下层。
可以。把源码贴给小布,它能标出缺失指针校验和标签缩进冲突的位置,并给出最小改动补丁建议。
用控件自带的缩进属性而非空格字符,并在每次宽度变化后做指针位置校验,防止缩进溢出可视区。
在图表大小变更事件里重算控件右边界坐标,按新宽度比例重排;落地时建议只锚定右侧控件,左侧保持固定降低复杂度。