图形界面 V: 垂直与水平滚动条 (第一章)·综合运用
📘

图形界面 V: 垂直与水平滚动条 (第一章)·综合运用

第 3/3 篇

「滚动条类的状态与位置接口」

在自定义控件里,滚动条需要对外暴露两组东西:一是拖拽结束后的视觉与状态复位,二是当前滚动位置与按钮状态的读取接口。下面这段摘出来的类定义,就是典型的最小可用封装。 松开鼠标左键且滑块不在按压态时,代码把 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 里打印日志,验证每次点箭头按钮状态翻转是否符合预期。

MQL5 / C++
  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 后拖一下滚动条,就能看到滑块被夹在区域内的效果。外汇与贵金属图表插件开发属高风险改造,参数越界可能引发图表对象异常。

MQL5 / C++
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 表示事件被滚动条吃掉。外汇与贵金属图表挂这类自定义控件时,需注意高频拖拽下重绘开销偏高,滑点风险也随界面卡顿上升。

MQL5 / C++
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 面板类里,挂上对象名就能在图表上验证滑块是否会卡边。外汇与贵金属图表挂自定义控件属高风险操作,参数不对可能遮挡报价区。

MQL5 / C++
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 自动化本身不杠杆风险,但策略执行层仍属高风险,验证时请用模拟账户。

常见问题

调用滚动条类暴露的 GetScrollPos() 一类状态接口读取当前位置值,再映射到你的列表行索引即可。
在派生类的拖拽逻辑里加坐标越界钳制,把滑块 top 限制在 [轨道顶, 轨道底-滑块高] 区间内。
可以,把控件代码贴给小布,它能对照滑块坐标与按钮点击逻辑帮你指出越界或联动断裂点。
通常是按钮点击回调里只改了固定步长没触发重绘,需在点击后同步刷新滑块坐标并重算视图偏移。
建议接着做列表视图与滚动条的组合封装,把视口裁剪和行渲染接进同一套坐标体系里。