图形界面 X: 简单快速开发库的更新 (版本 2)·进阶篇
「把鼠标坐标喂给界面控件」
在 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 把这段贴进类定义,拖一下面板就能验证光标跟随是否准。
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,表单外观参数被收进类内部而非暴露给外部调用。
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 建面板时,若日志冒出“在创建控件之前,表单指针 … 是必需的”,就是这里拦的。
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 用不同模板验证。
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; }