图形界面 VI: 滑动条与双重滑动条控件(第二章)·进阶篇
(2/3)·从零写出可复用滑动条类的第六步,很多自定义面板卡在滑块事件绑定上
面板控件配色与滑块尺寸的 setter 接口
在 MT5 自定义面板库里,这一组 void 方法专门用来在运行时改控件外观,不涉及任何返回判断,调用即写入成员变量。 输入栏位区分了普通、锁定两种状态:EditColor 管背景、EditTextColor 管文字、EditBorderColor 管边框,另外还有 Hover 态边框色,合计 7 个 setter 覆盖输入区全部视觉分支。 滑动条部分用 SlotLineDarkColor / SlotLineLightColor 控制沟槽双色,SlotIndicatorColor 控制指示针,ThumbXSize / ThumbYSize 接收 int 型像素尺寸,ThumbColor 接收 color 型滑块色——这意味着滑块长宽可独立设,不强制正方形。 直接把这些方法塞进 OnInit 之后的样式初始化函数,就能让 EA 面板在黄金和欧美盘不同波动时段切换配色,外汇与贵金属杠杆高,视觉误读可能放大操作风险。
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 实例拖一下就能复现。
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,这就是步长吸附的可验证现象。外汇与贵金属图表挂这类自定义控件时需注意,盘口跳动快,误触滑杆改参数可能引发高风险操作。
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 把这两段抄进你的面板类,拖一下就能验证数值跳变点是否和边界吻合。
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) 处理像素级位移。外汇贵金属面板做自定义滑条时,这套状态机可直接抄,但需注意对象名冲突导致的多实例重叠风险。
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,供后续命中测试使用。外汇与贵金属图表上挂这类自定义控件,需意识到滑点与重绘延迟可能让回写值偏离预期,属高风险操作。
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); } }