图形界面 V: 垂直与水平滚动条 (第一章)·综合运用
「滚动条类的状态与位置接口」
在自定义控件里,滚动条需要对外暴露两组东西:一是拖拽结束后的视觉与状态复位,二是当前滚动位置与按钮状态的读取接口。下面这段摘出来的类定义,就是典型的最小可用封装。 松开鼠标左键且滑块不在按压态时,代码把 m_scroll_state 置 false,并把滑块背景与边框色还原成默认。这个复位动作避免滑块卡在『按下』高亮态,是 GUI 交互里容易漏的一行。 CScroll 类继承自 CElement,protected 里只放了 m_scroll_state(bool)和 m_current_pos(int)两个核心字段。public 里用同名函数重载实现 getter/setter:比如 CurrentPos() 无参返回 m_current_pos,带 int 参则写入。ScrollIncState()、ScrollDecState() 直接转发内部增减按钮的 State(),外部轮询就能知道用户点在加还是减。 在 MT5 里新建一个空 EA,把这段类定义贴进 include 的自定义控件头文件,编译后你可以用 ScrollIncState() 在 OnChartEvent 里打印日志,验证每次点箭头按钮状态翻转是否符合预期。
class=class="str">"cmt">//--- 松开了鼠标左键 if(m_clamping_area_mouse==THUMB_NOT_PRESSED) { m_scroll_state=false; m_thumb.BackColor(m_thumb_color); m_thumb.Color(m_thumb_border_color); } } } class CScroll : class="kw">public CElement { class="kw">protected: class=class="str">"cmt">//--- 定义滚动条的状态 class="type">bool m_scroll_state; class=class="str">"cmt">//--- 滑轨的当前位置 class="type">int m_current_pos; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 按钮对象的名称 class="type">class="kw">string ScrollIncName(class="type">void) class="kw">const { class="kw">return(m_inc.Name()); } class="type">class="kw">string ScrollDecName(class="type">void) class="kw">const { class="kw">return(m_dec.Name()); } class=class="str">"cmt">//--- 按钮的状态 class="type">bool ScrollIncState(class="type">void) class="kw">const { class="kw">return(m_inc.State()); } class="type">bool ScrollDecState(class="type">void) class="kw">const { class="kw">return(m_dec.State()); } class=class="str">"cmt">//--- 滚动条的状态 class="type">void ScrollState(class="kw">const class="type">bool scroll_state) { m_scroll_state=scroll_state; } class="type">bool ScrollState(class="type">void) class="kw">const { class="kw">return(m_scroll_state); } class=class="str">"cmt">//--- 滑轨的当前位置 class="type">void CurrentPos(class="kw">const class="type">int pos) { m_current_pos=pos; } class="type">int CurrentPos(class="type">void) class="kw">const { class="kw">return(m_current_pos); } };
◍ 垂直滚动条派生类的内部拖拽逻辑
基类 CScroll 只提供创建对象、读写参数的通用接口,真正管事的是派生类。Scrolls.mqh 里写了 CScrollV(垂直)和 CScrollH(水平)两个派生类,二者逻辑几乎一致,区别仅在坐标轴:垂直条算 Y,水平条算 X。 CScrollV 里 OnDragThumb() 和 UpdateThumb() 被声明在 private 段,只供类内部调用。OnDragThumb() 负责识别用户是否真的在拖滑轨——鼠标在滑轨上按下且 Y 轴有位移,就视为拖拽开始;算出的 Y 值交给 UpdateThumb() 去刷新滑块坐标。 UpdateThumb() 会先做边界校验:如果拖拽超出了工作区,就把坐标夹回合法范围,再写回基类字段。这种「先算后校再存」的顺序,能避免滑块飞出控件可视区。 下面这段 OnDragThumb() 是拖拽识别的核心。它用 m_thumb_point_fixing 存按下时的光标 Y,用 m_thumb_size_fixing 存滑块边缘到光标的偏移;当 y 相对固定点大于 0 向下超阈、或小于 0 向上超阈时,分别算出 new_y_point 并调 UpdateThumb()。 在 MT5 里把这段贴进 EA 的自定义控件工程,挂上 OnEvent 后拖一下滚动条,就能看到滑块被夹在区域内的效果。外汇与贵金属图表插件开发属高风险改造,参数越界可能引发图表对象异常。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于管理垂直滚动条的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CScrollV : class="kw">public CScroll { class="kw">private: class=class="str">"cmt">//--- 移动滑轨 class="type">void OnDragThumb(class="kw">const class="type">int y); class=class="str">"cmt">//--- 更新滑轨的位置 class="type">void UpdateThumb(class="kw">const class="type">int new_y_point); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 移动滑轨 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollV::OnDragThumb(class="kw">const class="type">int y) { class=class="str">"cmt">//--- 为识别新的 Y 坐标 class="type">int new_y_point=class="num">0; class=class="str">"cmt">//--- 如果滚动条是不活动的, ... if(!CScroll::ScrollState()) { class=class="str">"cmt">//--- ...把用于移动滑轨的辅助变量清零 CScroll::m_thumb_size_fixing =class="num">0; CScroll::m_thumb_point_fixing =class="num">0; class="kw">return; } class=class="str">"cmt">//--- 如果固定点是零,保存光标当前的坐标 if(CScroll::m_thumb_point_fixing==class="num">0) CScroll::m_thumb_point_fixing=y; class=class="str">"cmt">//--- 如果滑轨边缘距离鼠标光标当前坐标距离为零,就进行计算 if(CScroll::m_thumb_size_fixing==class="num">0) CScroll::m_thumb_size_fixing=m_thumb.Y()-y; class=class="str">"cmt">//--- 如果在按下状态超过了向下的阈值 if(y-CScroll::m_thumb_point_fixing>class="num">0) { class=class="str">"cmt">//--- 计算 Y 坐标 new_y_point=y+CScroll::m_thumb_size_fixing; class=class="str">"cmt">//--- 更新滑轨的位置 UpdateThumb(new_y_point); class="kw">return; } class=class="str">"cmt">//--- 如果在按下状态超过了向上的阈值 if(y-CScroll::m_thumb_point_fixing<class="num">0) { class=class="str">"cmt">//--- 计算 Y 坐标 new_y_point=y-::fabs(CScroll::m_thumb_size_fixing); class=class="str">"cmt">//--- 更新滑轨的位置 UpdateThumb(new_y_point); class="kw">return; } }
垂直滚动条滑块越界与拖拽联动
在 MT5 自定义控件里,垂直滚动条 CScrollV 的滑块(thumb)位置必须被约束在背景区域 m_bg 内,否则会出现滑块跑到轨道外、CurrentPos 与视觉错位的问题。UpdateThumb 接收鼠标拖到的 y 坐标,先清掉固定点标记,再分别处理向下越界(超过 Y2 减滑块长度)和向上越界(小于等于 Y)两种情形,越界时直接把 y 钳到边界并重置 CurrentPos 为总步数或 0。 CalculateThumbPos 则是反向计算:用滑块当前 Y 减去背景 Y 再除以步长 m_thumb_step_size 得到位置索引;若滑块底沿到了 Y2-1 附近,就强制设为总步数减 1,避免浮点累积导致末位选不中。这里有个细节,步长为 0 时直接 return,防止除零崩溃。 ScrollBarControl 把鼠标坐标和按键状态接进来,先判焦点(x、y 是否落在 thumb 矩形内),再借 ScrollState 决定是否进入拖拽分支。一旦进入就调 OnDragThumb(y) 移动滑块并立刻 CalculateThumbPos 刷新数值,返回 true 表示事件被滚动条吃掉。外汇与贵金属图表挂这类自定义控件时,需注意高频拖拽下重绘开销偏高,滑点风险也随界面卡顿上升。
class="type">void CScrollV::UpdateThumb(class="kw">const class="type">int new_y_point) { class="type">int y=new_y_point; class=class="str">"cmt">//--- 固定点清零 CScroll::m_thumb_point_fixing=class="num">0; class=class="str">"cmt">//--- 检查是否向下超过工作区域并做调整 if(new_y_point>m_bg.Y2()-CScroll::m_thumb_length) { y=m_bg.Y2()-CScroll::m_thumb_length; CScroll::CurrentPos(class="type">int(CScroll::m_thumb_steps_total)); } class=class="str">"cmt">//--- 检查是否向上超过工作区域并作调整 if(new_y_point<=m_bg.Y()) { y=m_bg.Y(); CScroll::CurrentPos(class="num">0); } class=class="str">"cmt">//--- 更新坐标和边缘 m_thumb.Y(y); m_thumb.Y_Distance(y); m_thumb.YGap(m_thumb.Y()-(CElement::Y()-CElement::YGap())); } class CScrollV : class="kw">public CScroll { class="kw">private: class=class="str">"cmt">//--- 修正滑轨滑块的位置 class="type">void CalculateThumbPos(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改滑轨滑块位置数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollV::CalculateThumbPos(class="type">void) { class=class="str">"cmt">//--- 如果步长为零,退出 if(CScroll::m_thumb_step_size==class="num">0) class="kw">return; class=class="str">"cmt">//--- 修正滚动条位置的数值 CScroll::CurrentPos(class="type">int((m_thumb.Y()-m_bg.Y())/CScroll::m_thumb_step_size)); class=class="str">"cmt">//--- 检查是否向上/向下超出了工作区域 if(m_thumb.Y2()>=m_bg.Y2()-class="num">1) CScroll::CurrentPos(class="type">int(CScroll::m_thumb_steps_total-class="num">1)); if(m_thumb.Y()<m_bg.Y()) CScroll::CurrentPos(class="num">0); } class CScrollV : class="kw">public CScroll { class="kw">public: class=class="str">"cmt">//--- 管理滚动条 class="type">bool ScrollBarControl(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool mouse_state); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 管理滚动条 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CScrollV::ScrollBarControl(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool mouse_state) { class=class="str">"cmt">//--- 检查滚动条的焦点 m_thumb.MouseFocus(x>m_thumb.X() && x<m_thumb.X2() && y>m_thumb.Y() && y<m_thumb.Y2()); class=class="str">"cmt">//--- 检查并保存鼠标按键的状态 CScroll::CheckMouseButtonState(mouse_state); class=class="str">"cmt">//--- 改变滚动条的颜色 CScroll::ChangeObjectsColor(); class=class="str">"cmt">//--- 如果管理被传到滚动条,识别滚动条的位置 if(CScroll::ScrollState()) { class=class="str">"cmt">//--- 移动滑轨 OnDragThumb(y); class=class="str">"cmt">//--- 改变滚动条的位置 CalculateThumbPos(); class="kw">return(true); } class=class="str">"cmt">//--- class="kw">return(false); } class CScrollV : class="kw">public CScroll { class="kw">public: class=class="str">"cmt">//--- 计算滚动条的Y坐标 class="type">void CalculateThumbY(class="type">void); };
「垂直滚动条滑块坐标与按钮点击逻辑」
在 MT5 自定义控件里,垂直滚动条 CScrollV 的滑块位置必须实时绑定背景区域与当前步数,否则拖到头会穿帮。CalculateThumbY 就是干这个的:先用背景 Y 加上「当前位置 × 单步像素」算出原始 Y,再做上下越界钳制。 越界判断有两个硬条件:滑块 Y 小于背景顶就拉回顶;滑块底超过背景底,或当前步数已到 m_thumb_steps_total-1,就强制贴在底部减滑块长。钳制完把 Y、Y_Distance、YGap 三个属性一次性更新,滑块才不漂移。 点击响应分两支:OnClickScrollInc 对应向上按钮,只有对象名匹配、滚动条空闲、总步数≥1 才进逻辑,且当前位置大于 0 才自减;OnClickScrollDec 反之,位置小于总步数-1 才自增。两者都调 CalculateThumbY 重算坐标,并置对应按钮 State(true)。 直接把下面代码贴进你的 EA 面板类里,挂上对象名就能在图表上验证滑块是否会卡边。外汇与贵金属图表挂自定义控件属高风险操作,参数不对可能遮挡报价区。
class=class="str">"cmt">//| 计算滚动条的Y坐标 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollV::CalculateThumbY(class="type">void) { class=class="str">"cmt">//--- 识别当前滚动条的 Y 坐标 class="type">int scroll_thumb_y=class="type">int(m_bg.Y()+(CScroll::CurrentPos()*CScroll::m_thumb_step_size)); class=class="str">"cmt">//--- 如果向上超出了工作区域 if(scroll_thumb_y<=m_bg.Y()) scroll_thumb_y=m_bg.Y(); class=class="str">"cmt">//--- 如果向下超出了工作区域u if(scroll_thumb_y+CScroll::m_thumb_length>=m_bg.Y2() || CScroll::CurrentPos()>=CScroll::m_thumb_steps_total-class="num">1) { scroll_thumb_y=class="type">int(m_bg.Y2()-CScroll::m_thumb_length); } class=class="str">"cmt">//--- 更新在Y轴的坐标和边缘 m_thumb.Y(scroll_thumb_y); m_thumb.Y_Distance(scroll_thumb_y); m_thumb.YGap(m_thumb.Y()-m_wnd.Y()); } class CScrollV : class="kw">public CScroll { class="kw">public: class=class="str">"cmt">//--- 处理滚动条按钮被按下的情况 class="type">bool OnClickScrollInc(class="kw">const class="type">class="kw">string clicked_object); class="type">bool OnClickScrollDec(class="kw">const class="type">class="kw">string clicked_object); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下向上/向左按钮 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CScrollV::OnClickScrollInc(class="kw">const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果不是按下此对象或者滚动条不是活动的或者没有步数,就退出 if(m_inc.Name()!=clicked_object || CScroll::ScrollState() || CScroll::m_thumb_steps_total<class="num">1) class="kw">return(false); class=class="str">"cmt">//--- 减小滚动条的位置 if(CScroll::CurrentPos()>class="num">0) CScroll::m_current_pos--; class=class="str">"cmt">//--- 计算滚动条的Y坐标 CalculateThumbY(); class=class="str">"cmt">//--- 设置按下状态 m_inc.State(true); class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 处理按下向下/向右按钮 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CScrollV::OnClickScrollDec(class="kw">const class="type">class="kw">string clicked_object) { class=class="str">"cmt">//--- 如果不是按下此对象或者滚动条不是活动的或者没有步数,就退出 if(m_dec.Name()!=clicked_object || CScroll::ScrollState() || CScroll::m_thumb_steps_total<class="num">1) class="kw">return(false); class=class="str">"cmt">//--- 增加滚动条位置值 if(CScroll::CurrentPos()<CScroll::m_thumb_steps_total-class="num">1) CScroll::m_current_pos++; class=class="str">"cmt">//--- 计算滚动条的Y坐标 CalculateThumbY(); class=class="str">"cmt">//--- 设置按下状态 m_dec.State(true); class="kw">return(true); }
◍ 列表视图之后接着啃哪块骨头
这一节把滚动条控件收了尾,但 GUI 库的开发并没有停。下一篇要落地的是列表视图(list view),它的项目数一旦超出可视区域,就直接复用本章写好的垂直滚动条类,而不是另起炉灶。
目前第五部分提供的工程包有两个:easyandfastgui_mql4.zip 约 196.34 KB,easyandfastgui_mql5.zip 约 196.3 KB,解压后能在 MT5 里直接编译跑通,验证滚动条和后续列表视图的衔接逻辑。
如果你在 MT5 里加载这些文件后发现控件显示异常,优先检查 TestLibrary.mqh 的包含路径,社区里已有用户反馈过类似编译与环境问题。外汇与贵金属 GUI 自动化本身不杠杆风险,但策略执行层仍属高风险,验证时请用模拟账户。