图形界面 V: 垂直与水平滚动条 (第一章)·进阶篇
(2/3)· 列表视图装不下数据时,滚动条类架构错了就会处处补丁,这篇先把骨架立稳
「滚动条配色与图标接口的底层绑定」
自定义滚动条类里,边框和背景色并非直接写死在绘制函数,而是透过一组 setter 把颜色存进成员变量。AreaBorderColor 与 BgBorderColor 分别接管滑轨区域边框、背景边框;改这两个值,MT5 控件重绘时才会换肤。 按钮图标走的是文件路径注入,不是内嵌位图。IncFileOn / IncFileOff / DecFileOn / DecFileOff 四个接口接收 string 路径,分别指向增量键 hover 与非 hover、减量键两套状态图。留空字符串时对应状态不画图标,这是调试界面闪烁时常漏看的点。 Thumb 系列更细:ThumbColor 管滑块底色,ThumbColorHover / ThumbColorPressed 管鼠标悬停与按下的底色;边框同理拆成 BorderColor 与带 Hover、Pressed 后缀的三态。三态若只设了默认态,滑上去颜色不会变,容易误以为控件坏了。 构造函数把 m_area_width 初始化为 15,即滑轨默认宽 15 像素;m_area_length、m_thumb_width 初始为 0,意味着不主动算长度,滑块宽度就是塌的。开盘前若没调过这几个值,滚动条可能不可见。外汇与贵金属图表挂这类自定义控件属高风险改造,参数错乱可能卡 UI,建议在模拟账户先跑。
class="type">void AreaBorderColor(class="kw">const class="type">class="kw">color clr) { m_area_border_color=clr; } class="type">void BgBorderColor(class="kw">const class="type">class="kw">color clr) { m_bg_border_color=clr; } class=class="str">"cmt">//--- 设置按钮图标 class="type">void IncFileOn(class="kw">const class="type">class="kw">string file_path) { m_inc_file_on=file_path; } class="type">void IncFileOff(class="kw">const class="type">class="kw">string file_path){ m_inc_file_off=file_path; } class="type">void DecFileOn(class="kw">const class="type">class="kw">string file_path) { m_dec_file_on=file_path; } class="type">void DecFileOff(class="kw">const class="type">class="kw">string file_path){ m_dec_file_off=file_path; } class=class="str">"cmt">//--- (class="num">1) 滑轨背景的颜色 以及(class="num">2)滑轨背景边框的颜色 class="type">void ThumbColor(class="kw">const class="type">class="kw">color clr) { m_thumb_border_color=clr; } class="type">void ThumbColorHover(class="kw">const class="type">class="kw">color clr) { m_thumb_border_color_hover=clr; } class="type">void ThumbColorPressed(class="kw">const class="type">class="kw">color clr) { m_thumb_border_color_pressed=clr; } class="type">void ThumbBorderColor(class="kw">const class="type">class="kw">color clr) { m_thumb_border_color=clr; } class="type">void ThumbBorderColorHover(class="kw">const class="type">class="kw">color clr) { m_thumb_border_color_hover=clr; } class="type">void ThumbBorderColorPressed(class="kw">const class="type">class="kw">color clr){ m_thumb_border_color_pressed=clr; } }; CScroll::CScroll(class="type">void) : m_area_width(class="num">15), m_area_length(class="num">0), m_inc_file_on(""), m_inc_file_off(""), m_dec_file_on(""), m_dec_file_off(""), m_thumb_width(class="num">0),
◍ 滚动条类的初始化与图层优先级
在 MT5 自定义控件里,CScroll 的构造函数用初始化列表把滑块最小长度锁死在 15 像素,背景与滑块配色全部用 C'R,G,B' 整型色值写死,例如滑块常态 C'190,190,190'、按下态 C'160,160,160'。这样的硬编码适合做统一风格的面板,但若要做深色主题得逐行改。 构造函数体内紧接着给四个图层的 zorder 赋值:滑动区 8、背景 9、箭头 10、滑块 11。数字越大越靠前,意味着鼠标点击判定会优先落在滑块和箭头上,避免背景吞掉事件。 类声明里把 m_area、m_bg、m_inc、m_dec、m_thumb 全部保护起来,对外只暴露 CreateScroll(chart_id, subwin, x, y, items_total, visible_items_total)。后两个参数决定内容总量与可视量,是滑块长度计算的输入,开 MT5 写个测试面板传不同值就能看到滑块比例变化。 CreateBg 内部先拼对象名,依据类名判断是竖滚还是横滚:CScrollV 用 '_scrollv_bg_' 前缀,否则 '_scrollh_bg_'。这一行决定了同图表里多个滚动条不会重名冲突,复制代码时别顺手改了前缀规则。
m_thumb_length(class="num">0), m_thumb_min_length(class="num">15), m_area_color(C&class="macro">#x27;class="num">210,class="num">210,class="num">210&class="macro">#x27;), m_area_border_color(C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;), m_bg_border_color(C&class="macro">#x27;class="num">210,class="num">210,class="num">210&class="macro">#x27;), m_thumb_color(C&class="macro">#x27;class="num">190,class="num">190,class="num">190&class="macro">#x27;), m_thumb_color_hover(C&class="macro">#x27;class="num">180,class="num">180,class="num">180&class="macro">#x27;), m_thumb_color_pressed(C&class="macro">#x27;class="num">160,class="num">160,class="num">160&class="macro">#x27;), m_thumb_border_color(C&class="macro">#x27;class="num">170,class="num">170,class="num">170&class="macro">#x27;), m_thumb_border_color_hover(C&class="macro">#x27;class="num">160,class="num">160,class="num">160&class="macro">#x27;), m_thumb_border_color_pressed(C&class="macro">#x27;class="num">140,class="num">140,class="num">140&class="macro">#x27;) { class=class="str">"cmt">//--- 设置鼠标左键点击的优先级 m_area_zorder =class="num">8; m_bg_zorder =class="num">9; m_arrow_zorder =class="num">10; m_thumb_zorder =class="num">11; } class CScroll : class="kw">public CElement { class="kw">protected: class=class="str">"cmt">//--- 用于创建滚动条的对象 CRectLabel m_area; CRectLabel m_bg; CBmpLabel m_inc; CBmpLabel m_dec; CRectLabel m_thumb; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 用于创建滚动条的方法 class="type">bool CreateScroll(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int items_total,class="kw">const class="type">int visible_items_total); class=class="str">"cmt">//--- class="kw">private: class="type">bool CreateArea(class="type">void); class="type">bool CreateBg(class="type">void); class="type">bool CreateInc(class="type">void); class="type">bool CreateDec(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 CScroll::CreateBg(class="type">void) { class=class="str">"cmt">//--- 构造对象名称 class="type">class="kw">string name =""; class="type">class="kw">string name_part =(CElement::ClassName()=="CScrollV")? "_scrollv_bg_" : "_scrollh_bg_"; class=class="str">"cmt">//--- 如果没有指定索引
滚动条背景与滑轨长度的实算逻辑
在自定义 GUI 的滚动条类里,背景对象 m_bg 的创建依赖类名分支:竖向滚动条 CScrollV 取 Y 方向可用长度减两倍滑块宽再减 2 作为滑轨区长 m_bg_length,横向则按 X 方向算。这个减 2 是边框占位,改 m_thumb_width 会直接改变 x/y 偏移与背景尺寸,开 MT5 调参能立刻看到滑块贴边变化。 滑轨步长用 (m_bg_length - m_thumb_min_length) / m_thumb_steps_total 算,若结果小于 1 则强制为 1,避免整数坐标下卡死不动。当 work_area 小于背景长时,滑块长 = 背景长 - work_area + 步长,否则退化为最小长度 m_thumb_min_length,这段代码决定了拖拽手感是否跟手。 下面这段是背景创建与滑轨计算的核心片段,注意 CElement::Index() 为 WRONG_VALUE 时用 Id() 命名,否则拼上 Index 和 Id,避免同程序多实例对象名冲突。外汇与贵金属图表上挂这类自定义控件需注意:高频刷新可能触发对象重绘卡顿,属高风险定制用法。
if(CElement::Index()==WRONG_VALUE) name=CElement::ProgramName()+name_part+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 如果已经指定了索引 else name=CElement::ProgramName()+name_part+(class="type">class="kw">string)CElement::Index()+"__"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 坐标 class="type">int x=class="num">0; class="type">int y=class="num">0; class=class="str">"cmt">//--- 大小 class="type">int x_size=class="num">0; class="type">int y_size=class="num">0; class=class="str">"cmt">//--- 根据滚动条类型设置属性 if(CElement::ClassName()=="CScrollV") { m_bg_length =CElement::YSize()-(m_thumb_width*class="num">2)-class="num">2; x =CElement::X()+class="num">1; y =CElement::Y()+m_thumb_width+class="num">1; x_size =m_thumb_width; y_size =m_bg_length; } else { m_bg_length =CElement::XSize()-(m_thumb_width*class="num">2)-class="num">2; x =CElement::X()+m_thumb_width+class="num">1; y =CElement::Y()+class="num">1; x_size =m_bg_length; y_size =m_thumb_width; } class=class="str">"cmt">//--- 创建对象 if(!m_bg.Create(m_chart_id,name,m_subwin,x,y,x_size,y_size)) class="kw">return(false); class=class="str">"cmt">//--- 设置属性 m_bg.BackColor(m_area_color); m_bg.Color(m_bg_border_color); m_bg.BorderType(BORDER_FLAT); m_bg.Corner(m_corner); m_bg.Selectable(false); m_bg.Z_Order(m_bg_zorder); m_bg.Tooltip("\n"); class=class="str">"cmt">//--- 保存坐标 m_bg.X(x); m_bg.Y(y); class=class="str">"cmt">//--- 保存边缘 m_bg.XGap(x-m_wnd.X()); m_bg.YGap(y-m_wnd.Y()); class=class="str">"cmt">//--- 保存大小 m_bg.XSize(x_size); m_bg.YSize(y_size); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_bg); class="kw">return(true); } class CScroll : class="kw">public CElement { class="kw">protected: class="kw">public: class=class="str">"cmt">//--- 计算滚动条滑轨的长度 class="type">bool CalculateThumbSize(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算滚动条滑轨的长度 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CScroll::CalculateThumbSize(class="type">void) { class=class="str">"cmt">//--- 如果移动滑轨的区域长度小于最小滑轨长度,就不需要计算 if(m_bg_length<m_thumb_min_length) class="kw">return(false); class=class="str">"cmt">//--- 计算滑轨步长 m_thumb_step_size=(class="type">class="kw">double)(m_bg_length-m_thumb_min_length)/m_thumb_steps_total; class=class="str">"cmt">//--- 步长不能小于1 m_thumb_step_size=(m_thumb_step_size<class="num">1)? class="num">1 : m_thumb_step_size; class=class="str">"cmt">//--- 计算用于移动滑轨的工作区域的大小 class="type">class="kw">double work_area=m_thumb_step_size*m_thumb_steps_total; class=class="str">"cmt">//--- 如果工作区域的大小小于整个区域的大小,取得滑轨的大小,否则设置最小值 class="type">class="kw">double thumb_size=(work_area<m_bg_length)? m_bg_length-work_area+m_thumb_step_size : m_thumb_min_length; class=class="str">"cmt">//--- 使用类型转换检查滑轨大小 m_thumb_length=((class="type">int)thumb_size<m_thumb_min_length)? m_thumb_min_length :(class="type">int)thumb_size;
「滚动条滑块的创建逻辑」
在 MT5 自定义控件里,滚动条滑块(thumb)的实例化由 CScroll::CreateThumb 完成。函数先拼出对象名:垂直滚动条带 _scrollv_thumb_ 后缀,水平滚动条带 _scrollh_thumb_,未指定 Index 时用 ProgramName+后缀+Id,指定了则用 ProgramName+后缀+Index+__+Id,避免同图表多实例重名。 坐标与尺寸按类型分支:垂直态 x 取滑块既有 X 或 m_x+1,y 取既有 Y 或 m_y+m_thumb_width+1,宽高锁定为 m_thumb_width 与 m_thumb_length;水平态则交换长宽角色,x 基准变成 m_x+m_thumb_width+1。 CalculateThumbSize 若返回 false 会直接 return(true) 跳过创建——这是个容易踩的坑,意味着外部必须先保证滑块尺寸算通。随后 m_thumb.Create 失败才返回 false,成功则写死 BORDER_FLAT、不可选中、Z 序与边距,并把指针存入数组。 外层 CScroll::CreateScroll 接收 chart_id、subwin、x、y、items_total、visible_items_total 六个参数;若 m_wnd 指针无效(POINTER_INVALID)会直接 Print 报错并中断,所以传参前务必用 CElement 基类绑好父窗口指针。
class="type">bool CScroll::CreateThumb(class="type">void) { class=class="str">"cmt">//--- 构造对象名称 class="type">class="kw">string name =""; class="type">class="kw">string name_part =(CElement::ClassName()=="CScrollV")? "_scrollv_thumb_" : "_scrollh_thumb_"; class=class="str">"cmt">//--- 如果没有指定索引 if(CElement::Index()==WRONG_VALUE) name=CElement::ProgramName()+name_part+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 如果已经指定了索引 else name=CElement::ProgramName()+name_part+(class="type">class="kw">string)CElement::Index()+"__"+(class="type">class="kw">string)CElement::Id(); class=class="str">"cmt">//--- 坐标 class="type">int x=class="num">0; class="type">int y=class="num">0; class=class="str">"cmt">//--- 大小 class="type">int x_size=class="num">0; class="type">int y_size=class="num">0; class=class="str">"cmt">//--- 计算滚动条的大小 if(!CalculateThumbSize()) class="kw">return(true); class=class="str">"cmt">//--- 根据滚动条类型设置属性 if(CElement::ClassName()=="CScrollV") { x =(m_thumb.X()>class="num">0) ? m_thumb.X() : m_x+class="num">1; y =(m_thumb.Y()>class="num">0) ? m_thumb.Y() : m_y+m_thumb_width+class="num">1; x_size =m_thumb_width; y_size =m_thumb_length; } else { x =(m_thumb.X()>class="num">0) ? m_thumb.X() : m_x+m_thumb_width+class="num">1; y =(m_thumb.Y()>class="num">0) ? m_thumb.Y() : m_y+class="num">1; x_size =m_thumb_length; y_size =m_thumb_width; } class=class="str">"cmt">//--- 创建对象 if(!m_thumb.Create(m_chart_id,name,m_subwin,x,y,x_size,y_size)) class="kw">return(false); class=class="str">"cmt">//--- 设置属性 m_thumb.BackColor(m_thumb_color); m_thumb.Color(m_thumb_border_color); m_thumb.BorderType(BORDER_FLAT); m_thumb.Corner(m_corner); m_thumb.Selectable(false); m_thumb.Z_Order(m_thumb_zorder); m_thumb.Tooltip("\n"); 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">//--- 保存大小 m_thumb.XSize(x_size); m_thumb.YSize(y_size); class=class="str">"cmt">//--- 保存对象指针 CElement::AddToArray(m_thumb); class="kw">return(true); } class="type">bool CScroll::CreateScroll(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">int items_total,class="kw">const class="type">int visible_items_total) { class=class="str">"cmt">//--- 如果没有表单指针 if(::CheckPointer(m_wnd)==POINTER_INVALID) { ::Print(__FUNCTION__," > 在创建滚动条之前, 类必须传入"
◍ 滚动条基类的初始化与鼠标状态枚举
CScroll 的初始化函数里先做了两道防护:表单指针取不到时直接返回 false;若基类 CElement 的 ClassName() 为空,说明有人想直接实例化基类,也立刻退出并提示必须用派生类 CScrollV 或 CScrollH。 变量初始化时按方向区分滑轨尺寸:竖向滚动条(CScrollV)用 XSize() 作宽度、YSize() 作长度,横向则反过来;滑块宽度固定为区域宽减 2,总步数 = 条目总数 - 可见条目数 + 1。例如 100 条数据、可见 10 条,m_thumb_steps_total 就是 91。 创建流程依次调 CreateArea / CreateBg / CreateInc / CreateDec / CreateThumb,任一步失败即终止。若所属窗口是对话框或已最小化,整体 Hide()。 鼠标交互侧定义了 ENUM_THUMB_MOUSE_STATE:0=未按下、1=在滑块外按下、2=在滑块内按下。CScroll 类暴露 CheckMouseButtonState(bool) 来记录按键区域,并用 ZeroThumbVariables() 在拖动结束后清零 m_thumb_size_fixing、m_thumb_point_fixing 等定位变量。 别拿基类当实例用 直接 new CScroll 会在运行时报错退出,MT5 终端里看到『使用滚动条的派生类』这句打印,就该把代码改成 CScrollV 或 CScrollH。
if(CElement::ClassName()=="") { ::Print(__FUNCTION__," > 使用滚动条的派生类 (CScrollV 或者 CScrollH)."); class="kw">return(false); } m_area_width =(CElement::ClassName()=="CScrollV")? CElement::XSize() : CElement::YSize(); m_area_length =(CElement::ClassName()=="CScrollV")? CElement::YSize() : CElement::XSize(); m_thumb_width =m_area_width-class="num">2; m_thumb_steps_total=items_total-visible_items_total+class="num">1; enum ENUM_THUMB_MOUSE_STATE { THUMB_NOT_PRESSED =class="num">0, THUMB_PRESSED_OUTSIDE =class="num">1, THUMB_PRESSED_INSIDE =class="num">2 }; class="type">void CScroll::CheckMouseButtonState(class="kw">const class="type">bool mouse_state) {
滚动条拖拽状态与配色联动的实现细节
在 MT5 自定义控件里,滚动条滑块(thumb)的按下、松开、移出区域都要靠 mouse_state 与 m_clamping_area_mouse 这套状态机区分。若鼠标未按下,直接调 ZeroThumbVariables() 清零并 return,避免后续误判。 按下时要先判断 m_clamping_area_mouse 是否已在别的区域置位,若是则直接退出,防止跨区重复触发。滑块在区域内按下会置为 THUMB_PRESSED_INSIDE,区域外则是 THUMB_PRESSED_OUTSIDE;非下拉类元件还会锁住所在的 CWindow 表单并记录激活 ID,保证拖动期间别的控件不抢焦点。 ZeroThumbVariables() 里把 m_thumb_size_fixing 设 0、m_clamping_area_mouse 回到 THUMB_NOT_PRESSED;非下拉元件同时解锁表单、把激活 ID 重置为 WRONG_VALUE。这一对函数是滑块交互干净退出的关键。 ChangeObjectsColor() 用 m_clamping_area_mouse 决定滑块配色:未按时用 hover 色,区域内按下切到 pressed 色。实测若漏掉 IsLocked() 判断,多控件同屏时会出现 A 控件滚动条染上 B 控件颜色的串色现象,开 MT5 拖一下就能复现。
if(!mouse_state) { class=class="str">"cmt">//--- 变量清零 ZeroThumbVariables(); class="kw">return; } class=class="str">"cmt">//--- 如果已按下 if(mouse_state) { class=class="str">"cmt">//--- 如果按键在另一个区域内按下,就退出 if(m_clamping_area_mouse!=THUMB_NOT_PRESSED) class="kw">return; class=class="str">"cmt">//--- 在滑轨区域之外 if(!m_thumb.MouseFocus()) m_clamping_area_mouse=THUMB_PRESSED_OUTSIDE; class=class="str">"cmt">//--- 在滑轨区域之内 else { m_clamping_area_mouse=THUMB_PRESSED_INSIDE; class=class="str">"cmt">//--- 如果不是下拉元件 if(!CElement::IsDropdown()) { class=class="str">"cmt">//--- 屏蔽表单并保存活动元件的ID m_wnd.IsLocked(true); m_wnd.IdActivatedElement(CElement::Id()); } } } class="type">void CScroll::ZeroThumbVariables(class="type">void) { class=class="str">"cmt">//--- 如果不是一个下拉元件 if(!CElement::IsDropdown()) { class=class="str">"cmt">//--- 解除表单屏蔽并重置活动元件ID m_wnd.IsLocked(false); m_wnd.IdActivatedElement(WRONG_VALUE); } m_thumb_size_fixing =class="num">0; m_clamping_area_mouse =THUMB_NOT_PRESSED; } class CScroll : class="kw">public CElement { class="kw">public: class=class="str">"cmt">//--- 改变滚动条对象的颜色 class="type">void ChangeObjectsColor(class="type">void); }; class="type">void CScroll::ChangeObjectsColor(class="type">void) { class=class="str">"cmt">//--- 如果表单被屏蔽并且当前活动的元件ID不同,就退出 if(m_wnd.IsLocked() && m_wnd.IdActivatedElement()!=CElement::Id()) class="kw">return; class=class="str">"cmt">//--- 列表视图滚动条按钮的颜色 if(!m_scroll_state) { m_inc.State(m_inc.MouseFocus()); m_dec.State(m_dec.MouseFocus()); } class=class="str">"cmt">//--- 如果光标在滚动条区域之内 if(m_thumb.MouseFocus()) { class=class="str">"cmt">//--- 如果松开了鼠标左键 if(m_clamping_area_mouse==THUMB_NOT_PRESSED) { m_scroll_state=false; m_thumb.BackColor(m_thumb_color_hover); m_thumb.Color(m_thumb_border_color_hover); } class=class="str">"cmt">//--- 在滑轨上按下了鼠标左键 else if(m_clamping_area_mouse==THUMB_PRESSED_INSIDE) { m_scroll_state=true; m_thumb.BackColor(m_thumb_color_pressed); m_thumb.Color(m_thumb_border_color_pressed); } } class=class="str">"cmt">//--- 如果光标在滚动条区域之外 else {