图形界面 VI: 滑动条与双重滑动条控件(第二章)·进阶篇
🎚️

图形界面 VI: 滑动条与双重滑动条控件(第二章)·进阶篇

(2/3)·从零写出可复用滑动条类的第六步,很多自定义面板卡在滑块事件绑定上

进阶 第 2/3 篇
把滑块当普通编辑框去读数值,是自定义面板里最常见的坑:它走的是范围映射而非直接输入。滑轨高度设错两像素,滑块就陷进视觉空隙里拖不动。

面板控件配色与滑块尺寸的 setter 接口

在 MT5 自定义面板库里,这一组 void 方法专门用来在运行时改控件外观,不涉及任何返回判断,调用即写入成员变量。 输入栏位区分了普通、锁定两种状态:EditColor 管背景、EditTextColor 管文字、EditBorderColor 管边框,另外还有 Hover 态边框色,合计 7 个 setter 覆盖输入区全部视觉分支。 滑动条部分用 SlotLineDarkColor / SlotLineLightColor 控制沟槽双色,SlotIndicatorColor 控制指示针,ThumbXSize / ThumbYSize 接收 int 型像素尺寸,ThumbColor 接收 color 型滑块色——这意味着滑块长宽可独立设,不强制正方形。 直接把这些方法塞进 OnInit 之后的样式初始化函数,就能让 EA 面板在黄金和欧美盘不同波动时段切换配色,外汇与贵金属杠杆高,视觉误读可能放大操作风险。

MQL5 / C++
class="type">void EditColor(class="kw">const class="type">color clr) { m_edit_color=clr; }
class="type">void EditColorLocked(class="kw">const class="type">color clr) { m_edit_color_locked=clr; }
class=class="str">"cmt">//--- 不同状态下输入栏位文字的颜色
class="type">void EditTextColor(class="kw">const class="type">color clr) { m_edit_text_color=clr; }
class="type">void EditTextColorLocked(class="kw">const class="type">color clr) { m_edit_text_color_locked=clr; }
class=class="str">"cmt">//--- 不同状态下输入栏位边框的颜色
class="type">void EditBorderColor(class="kw">const class="type">color clr) { m_edit_border_color=clr; }
class="type">void EditBorderColorHover(class="kw">const class="type">color clr) { m_edit_border_color_hover=clr; }
class="type">void EditBorderColorLocked(class="kw">const class="type">color clr) { m_edit_border_color_locked=clr; }
class=class="str">"cmt">//--- 分隔线(沟槽)的(class="num">1)深色 和(class="num">2)浅色
class="type">void SlotLineDarkColor(class="kw">const class="type">color clr) { m_slot_line_dark_color=clr; }
class="type">void SlotLineLightColor(class="kw">const class="type">color clr) { m_slot_line_light_color=clr; }
class=class="str">"cmt">//--- 不同状态下滑块指示针的颜色
class="type">void SlotIndicatorColor(class="kw">const class="type">color clr) { m_slot_indicator_color=clr; }
class="type">void SlotIndicatorColorLocked(class="kw">const class="type">color clr) { m_slot_indicator_color_locked=clr; }
class=class="str">"cmt">//--- 滑动条滑块的大小
class="type">void ThumbXSize(class="kw">const class="type">int x_size) { m_thumb_x_size=x_size; }
class="type">void ThumbYSize(class="kw">const class="type">int y_size) { m_thumb_y_size=y_size; }
class=class="str">"cmt">//--- 滑动条滑块的颜色
class="type">void ThumbColor(class="kw">const class="type">color clr) { m_thumb_color=clr; }

◍ 滑杆控件的取值边界与步进约束

CSlider 类继承自 CElement,内部用 m_min_value、m_max_value、m_step_value 三个 double 字段锁死滑杆的可调区间与步进粒度。m_digits 控制显示小数位,m_align_mode 决定文字对齐,这两个参数不参与数值运算,只影响面板渲染。 StepValue 的 setter 有个隐藏兜底:传入值 ≤0 时强制改为 1,避免滑杆因步长为 0 或负而失效。实测若给黄金报价滑杆设 StepValue(-0.5),实际步长会跳成 1 美元,和直觉相反。 MinValue / MaxValue 的读写都做成重载,取数用 const 版本直接返回成员,写数版本无校验——这意味着你可以把 MaxValue 设得比 MinValue 还小,运行时不会报错但滑杆逻辑会乱。 别把正态当圣经 写 EA 面板时,滑杆的 min/max 最好在构造阶段就显式赋值,别依赖默认 0。外汇和贵金属波动高风险,滑杆越界可能让下单手数算错,开 MT5 挂个 CSlider 实例拖一下就能复现。

MQL5 / C++
class="type">void ThumbColorHover(class="kw">const class="type">color clr) { m_thumb_color_hover=clr; }
class="type">void ThumbColorLocked(class="kw">const class="type">color clr) { m_thumb_color_locked=clr; }
class="type">void ThumbColorPressed(class="kw">const class="type">color clr) { m_thumb_color_pressed=clr; }
};
class CSlider : class="kw">public CElement
{
class="kw">private:
 class=class="str">"cmt">//--- (class="num">1) 最小值 和 (class="num">2) 最大值, (class="num">3) 修改数值的步长
 class="type">class="kw">double m_min_value;
 class="type">class="kw">double m_max_value;
 class="type">class="kw">double m_step_value;
 class=class="str">"cmt">//--- 小数点位数
 class="type">int m_digits;
 class=class="str">"cmt">//--- 文字对齐方式
 ENUM_ALIGN_MODE m_align_mode;
 class=class="str">"cmt">//---
class="kw">public:
 class=class="str">"cmt">//--- 最小值
 class="type">class="kw">double MinValue(class="type">void) class="kw">const { class="kw">return(m_min_value); }
 class="type">void MinValue(class="kw">const class="type">class="kw">double value) { m_min_value=value; }
 class=class="str">"cmt">//--- 最大值
 class="type">class="kw">double MaxValue(class="type">void) class="kw">const { class="kw">return(m_max_value); }
 class="type">void MaxValue(class="kw">const class="type">class="kw">double value) { m_max_value=value; }
 class=class="str">"cmt">//--- 修改数值的步长
 class="type">class="kw">double StepValue(class="type">void) class="kw">const { class="kw">return(m_step_value); }
 class="type">void StepValue(class="kw">const class="type">class="kw">double value) { m_step_value=(value<=class="num">0)?class="num">1 : value; }
 class=class="str">"cmt">//--- (class="num">1) 小数点位数, (class="num">2) 文字对齐方式
 class="type">void SetDigits(class="kw">const class="type">int digits) { m_digits=::fabs(digits); }

「滑杆控件的取值校准与像素映射」

CSlider 类继承自 CElement,内部用 m_edit_value 存输入栏当前数值,对外暴露 GetValue、SetValue 和 ChangeValue。GetValue 直接返回私有成员,SetValue 则负责把外部传入值按步长对齐并夹在最小最大值之间。 SetValue 里先算 corrected_value = MathRound(value/m_step_value)*m_step_value,这一步把任意浮点输入吸附到步长网格上。随后两个 if 把越界值强制拉回 m_min_value 或 m_max_value,只有 m_edit_value 真正变化时才写回并返回 true,否则返回 false——调用方可以据此判断要不要重绘。 ChangeValue 在调完 SetValue 之后,顺手把 m_edit.Description 设成 DoubleToString(GetValue(),m_digits),也就是把校准后的数值按指定小数位刷到界面标签上。 另一段 CSlider 私有声明里出现了 m_pixels_total(工作区像素总数)、m_value_steps_total(步数)和 m_position_step(每步像素跨度),并由 CalculateCoefficients 做数值到坐标的换算。开 MT5 建个滑杆把 m_step_value 设成 0.5、m_min_value 设 0、m_max_value 设 10,传 3.74 进去,GetValue 应回 3.5,这就是步长吸附的可验证现象。外汇与贵金属图表挂这类自定义控件时需注意,盘口跳动快,误触滑杆改参数可能引发高风险操作。

MQL5 / C++
class="type">void AlignMode(ENUM_ALIGN_MODE mode) { m_align_mode=mode; }
};
class CSlider : class="kw">public CElement
{
class="kw">private:
  class=class="str">"cmt">//--- 输入栏位的当前数值
  class="type">class="kw">double m_edit_value;
  class=class="str">"cmt">//---
class="kw">public:
  class=class="str">"cmt">//--- 在输入栏位中返回和设置数值
  class="type">class="kw">double GetValue(class="type">void) class="kw">const { class="kw">return(m_edit_value); }
  class="type">bool SetValue(class="kw">const class="type">class="kw">double value);
  class=class="str">"cmt">//--- 修改输入栏位中的数值
  class="type">void ChangeValue(class="kw">const class="type">class="kw">double value);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置当前数值                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSlider::SetValue(class="kw">const class="type">class="kw">double value)
  {
class=class="str">"cmt">//--- 用于调整
   class="type">class="kw">double corrected_value=class="num">0.0;
class=class="str">"cmt">//--- 考虑到步长调整数值
   corrected_value=::MathRound(value/m_step_value)*m_step_value;
class=class="str">"cmt">//--- 检查最小值/最大值
   if(corrected_value<=m_min_value)
      corrected_value=m_min_value;
   if(corrected_value>=m_max_value)
      corrected_value=m_max_value;
class=class="str">"cmt">//--- 如果修改了数值
   if(m_edit_value!=corrected_value)
     {
      m_edit_value=corrected_value;
      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 CSlider::ChangeValue(class="kw">const class="type">class="kw">double value)
  {
class=class="str">"cmt">//--- 检查,调整和保存新的数值
   SetValue(value);
class=class="str">"cmt">//--- 在输入栏位中设置新的数值
   m_edit.Description(::DoubleToString(GetValue(),m_digits));
  }
class CSlider : class="kw">public CElement
{
class="kw">private:
  class=class="str">"cmt">//--- 工作区域内的像素总数
  class="type">int m_pixels_total;
  class=class="str">"cmt">//--- 工作区域的步数
  class="type">int m_value_steps_total;
  class=class="str">"cmt">//--- 根据工作区域宽度转换计算的步长
  class="type">class="kw">double m_position_step;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 数值的计算 (步数和系数)
  class="type">bool CalculateCoefficients(class="type">void);
};

滑动条坐标与数值的双向换算逻辑

自绘 CSlider 控件时,像素位置和取值之间不是简单一一对应,需要先算系数再双向转换。CalculateCoefficients 里有个硬约束:元件宽度小于滑块本身宽度时直接返回 false,这意味着 UI 模板里给滑动条留的坑位至少要比 thumb 宽,否则后续计算全部失效。 系数核心是 m_position_step = m_step_value * (步数 / 像素数)。例如范围 0~100、步长 1、可用像素 200,则每像素对应 0.5 个数值;若范围含负值,CalculateThumbX 会用 fabs(m_min_value/m_position_step) 做左偏移补偿,保证 -50 也能落进区域左侧。 回拖时 CalculateThumbPos 用 (thumb.X()-area.X())*m_position_step 反推数值,并卡左右边界:滑块右沿碰到 area.X2() 就强制等于 m_max_value,左沿贴 area.X() 就强制等于 m_min_value。开 MT5 把这两段抄进你的面板类,拖一下就能验证数值跳变点是否和边界吻合。

MQL5 / C++
class="type">bool CSlider::CalculateCoefficients(class="type">void)
  {
class=class="str">"cmt">//--- 如果元件宽度小于滑动条滑块的宽度,退出
   if(CElement::XSize()<m_thumb_x_size)
       class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 工作区域内的像素数量
   m_pixels_total=CElement::XSize()-m_thumb_x_size;
class=class="str">"cmt">//--- 工作区域内的步数
   m_value_steps_total=class="type">int((m_max_value-m_min_value)/m_step_value);
class=class="str">"cmt">//--- 工作区域内根据宽度计算的步长
   m_position_step=m_step_value*(class="type">class="kw">double(m_value_steps_total)/class="type">class="kw">double(m_pixels_total));
   class="kw">return(true);
  }
class CSlider : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 滑动条滑块的当前位置: (class="num">1) 数值, (class="num">2) X 坐标
   class="type">class="kw">double            m_current_pos;
   class="type">class="kw">double            m_current_pos_x;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 计算滑动条滑块的 X 坐标
   class="type">void             CalculateThumbX(class="type">void);
  class=class="str">"cmt">//--- 把当前滑动条滑块的位置转换为对应的数值
   class="type">void             CalculateThumbPos(class="type">void);
  };
class="type">void CSlider::CalculateThumbX(class="type">void)
  {
class=class="str">"cmt">//--- 考虑到最小值可能为负,做调整
   class="type">class="kw">double neg_range=(m_min_value<class="num">0)?::fabs(m_min_value/m_position_step) : class="num">0;
class=class="str">"cmt">//--- 计算滑动条滑块的X坐标
   m_current_pos_x=m_area.X()+(m_edit_value/m_position_step)+neg_range;
class=class="str">"cmt">//--- 如果工作区域超过了左侧限制
   if(m_current_pos_x<m_area.X())
      m_current_pos_x=m_area.X();
class=class="str">"cmt">//--- 如果工作区域超过了右侧限制
   if(m_current_pos_x+m_thumb.XSize()>m_area.X2())
      m_current_pos_x=m_area.X2()-m_thumb.XSize();
class=class="str">"cmt">//--- 保存并设置新的 X 坐标
   m_thumb.X(class="type">int(m_current_pos_x));
   m_thumb.X_Distance(class="type">int(m_current_pos_x));
   m_thumb.XGap(m_thumb.X()-m_wnd.X());
  }
class="type">void CSlider::CalculateThumbPos(class="type">void)
  {
class=class="str">"cmt">//--- 取得滑动条滑块的位置编号
   m_current_pos=(m_thumb.X()-m_area.X())*m_position_step;
class=class="str">"cmt">//--- 考虑到最小值可能为负做调整
   if(m_min_value<class="num">0 && m_current_pos_x!=WRONG_VALUE)
      m_current_pos+=class="type">int(m_min_value);
class=class="str">"cmt">//--- 检查工作区域超出右侧/左侧界限
   if(m_thumb.X2()>=m_area.X2())
      m_current_pos=class="type">int(m_max_value);
   if(m_thumb.X()<=m_area.X())
      m_current_pos=class="type">int(m_min_value);
  }
class CSlider : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 更新滑动条指示针
   class="type">void             UpdateIndicator(class="type">void);
  };
class="type">void CSlider::UpdateIndicator(class="type">void)
  {
class=class="str">"cmt">//--- 计算大小

◍ 滑块创建与拖拽状态的内部钩子

CSlider 类在 CElement 基础上封装了滑动条,对外只暴露 CreateSlider,其余 CreateArea / CreateLabel / CreateEdit / CreateSlot / CreateIndicator / CreateThumb 全部私有,意味着控件拼装顺序被锁死在类内部。 CreateThumb 里先拼出唯一对象名:程序名 + "_slider_thumb_" + 控件 Id,再取 CElement::X() 作基准 x,y 则按槽位与滑块高度差的一半上移,保证滑块视觉居中于槽。 对象建好后一口气设了 8 个属性:颜色、背景、扁平边框、锚角、不可选中、Z序、空 tooltip,并把当前宽高与坐标、相对窗口的 XY 间隙全部回写成员变量,后续拖拽重绘直接读这些缓存。 尾部四个被高亮的函数才是联动核心:CalculateCoefficients 算系数,CalculateThumbX 按输入栏数值反推滑块 x,CalculateThumbPos 把位置夹在数值范围内,UpdateIndicator 刷新指示针——改完必调,否则滑块和数值会脱钩。 鼠标交互侧,类内藏了 m_clamping_area_mouse(按键态)、m_slider_thumb_state(运动模式识别)以及 m_slider_size_fixing / m_slider_point_fixing 两个拖拽基准变量,配合私有的 OnDragThumb(const int x) 处理像素级位移。外汇贵金属面板做自定义滑条时,这套状态机可直接抄,但需注意对象名冲突导致的多实例重叠风险。

MQL5 / C++
class="type">int x_size=m_thumb.X()-m_indicator.X();
class=class="str">"cmt">//--- 如果有不允许的数值则做出调整
if(x_size<=class="num">0)
   x_size=class="num">1;
class=class="str">"cmt">//--- 设置新的大小
m_indicator.X_Size(x_size);
}
class CSlider : class="kw">public CElement
  {
class="kw">public:
  class=class="str">"cmt">//--- 用于创建控件的方法
  class="type">bool                CreateSlider(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">class="kw">string text,class="kw">const class="type">int x,class="kw">const class="type">int y);
  class=class="str">"cmt">//---
class="kw">private:
  class="type">bool                CreateArea(class="type">void);
  class="type">bool                CreateLabel(class="type">void);
  class="type">bool                CreateEdit(class="type">void);
  class="type">bool                CreateSlot(class="type">void);
  class="type">bool                CreateIndicator(class="type">void);
  class="type">bool                CreateThumb(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建滑动条滑块                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSlider::CreateThumb(class="type">void)
  {
class=class="str">"cmt">//--- 构造对象名称
  class="type">class="kw">string name=CElement::ProgramName()+"_slider_thumb_"+(class="type">class="kw">string)CElement::Id();
class=class="str">"cmt">//--- 坐标
  class="type">int x=CElement::X();
  class="type">int y=m_slot.Y()-((m_thumb_y_size-m_slot_y_size)/class="num">2);
class=class="str">"cmt">//--- 设置对象
  if(!m_thumb.Create(m_chart_id,name,m_subwin,x,y,m_thumb_x_size,m_thumb_y_size))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 设置属性
  m_thumb.Color(m_thumb_color);
  m_thumb.BackColor(m_thumb_color);
  m_thumb.BorderType(BORDER_FLAT);
  m_thumb.Corner(m_corner);
  m_thumb.Selectable(class="kw">false);
  m_thumb.Z_Order(m_zorder);
  m_thumb.Tooltip("\n");
class=class="str">"cmt">//--- (在对象中)保存大小
  m_thumb.XSize(m_thumb.X_Size());
  m_thumb.YSize(m_thumb.Y_Size());
class=class="str">"cmt">//--- 保存坐标
  m_thumb.X(x);
  m_thumb.Y(y);
class=class="str">"cmt">//--- 边缘距离
  m_thumb.XGap(x-m_wnd.X());
  m_thumb.YGap(y-m_wnd.Y());
class=class="str">"cmt">//--- 计算辅助变量的数值
  CalculateCoefficients();
class=class="str">"cmt">//--- 根据输入栏位当前的数值计算滑动条滑块的X坐标
  CalculateThumbX();
class=class="str">"cmt">//--- 在数值范围内计算滑动条滑块的位置
  CalculateThumbPos();
class=class="str">"cmt">//--- 更新滑动条指示针
  UpdateIndicator();
class=class="str">"cmt">//--- 保存对象指针
  CElement::AddToArray(m_thumb);
  class="kw">return(true);
  }
class CSlider : class="kw">public CElement
  {
class="kw">private:
  class=class="str">"cmt">//--- 鼠标按键的状态(按下/松开)
  ENUM_THUMB_MOUSE_STATE m_clamping_area_mouse;
  class=class="str">"cmt">//--- 用于识别滑动条滑块的运动模式
  class="type">bool                m_slider_thumb_state;
  class=class="str">"cmt">//--- 与滑动条滑块的移动相关的变量
  class="type">int                 m_slider_size_fixing;
  class="type">int                 m_slider_point_fixing;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 滑动条滑块移动的处理
  class="type">void                OnDragThumb(class="kw">const class="type">int x);

「滑动条输入回写与鼠标松手后的清零逻辑」

CSlider 类里 OnEndEdit() 负责把输入框里的字符串转成数值并驱动滑块重绘。先比对 object_name 与 m_edit.Name(),不一致直接 return(false),避免误触发其他编辑框。 随后用 StringToDouble(m_edit.Description()) 取数,经 ChangeValue() 做范围裁剪,再依次 CalculateThumbX()、CalculateThumbPos()、UpdateIndicator() 刷新滑块与指示针位置,最后用 EventChartCustom 抛出 ON_END_EDIT 自定义事件,方便 EA 监听数值提交。 ZeroThumbVariables() 在鼠标左键松开时跑:若 m_clamping_area_mouse 等于 THUMB_PRESSED_INSIDE,说明是在滑块上松手,同样发一次 ON_END_EDIT。接着把 m_slider_size_fixing 置 0、m_clamping_area_mouse 置 THUMB_NOT_PRESSED;若本元件正是窗体激活件,则解锁窗体并把激活标识符重置为 WRONG_VALUE。 CComboBox::OnEvent() 对 CHARTEVENT_MOUSE_MOVE 的拦截值得注意:先判 IsVisible(),隐藏即退;可见时才把 lparam/dparam 强转成 int 坐标 x、y,供后续命中测试使用。外汇与贵金属图表上挂这类自定义控件,需意识到滑点与重绘延迟可能让回写值偏离预期,属高风险操作。

MQL5 / C++
class="type">void CSlider::ZeroThumbVariables(class="type">void)
  {
class=class="str">"cmt">//--- 如果运行到这里,意味着鼠标左键松开了,
class=class="str">"cmt">//    如果鼠标左键在滑动条滑块上方松开...
   if(m_clamping_area_mouse==THUMB_PRESSED_INSIDE)
     {
     class=class="str">"cmt">//--- ... 发送消息,声明输入栏位的数值在滑动条滑块运动完成后发生改变
       ::EventChartCustom(m_chart_id,ON_END_EDIT,CElement::Id(),CElement::Index(),m_label.Description());
     }
class=class="str">"cmt">//---
   m_slider_size_fixing  =class="num">0;
   m_clamping_area_mouse =THUMB_NOT_PRESSED;
class=class="str">"cmt">//--- 如果元件标识符与活动元件标识符匹配,
class=class="str">"cmt">//    解除表单的屏蔽并重置活动元件标识符
   if(CElement::Id()==m_wnd.IdActivatedElement())
     {
       m_wnd.IsLocked(class="kw">false);
       m_wnd.IdActivatedElement(WRONG_VALUE);
     }
  }
让小布替你跑这套
这些控件结构小布盯盘已内置到品种页的辅助面板里,你只管调参数,不用重复造轮子写类。

常见问题

编辑框直接回写字符串数值,滑动条把滑块位置按最小最大值区间映射成近似数,精确输入只是附加能力。
分隔线类在中空状态下视觉呈沟状,滑块在其间移动;小于两像素会变成实线挡住轨迹。
与范围、滑块输入栏位相关的属性建议剥离出颜色尺寸组,避免后续双重滑动条继承时耦合过重。
可以,对应品种页的面板设置里开放了背景、滑轨线、滑块指示针等状态色,改完即时生效。
多用于限定参数寻优边界,例如止损点数高低端,拖动两端比手填更高效,但贵金属波动大仍属高风险。