DoEasy.控件(第 33 部分):垂直滚动条(基础篇)
📘

DoEasy.控件(第 33 部分):垂直滚动条(基础篇)

第 1/3 篇

给库加上垂直滚动条控件

在 DoEasy 控件系列的第 33 部分里,核心动作是为现有库补上垂直滚动条(Vertical ScrollBar)这一基础交互组件,让基于库绘制的面板在内容超出可视区时能够上下滚动。 原文示例发布于 2024 年 11 月 12 日 10:00,截至整理时该文在源站获得 810 次阅读与 3 条读者反馈,说明这类底层控件改造在 MT5 自建 UI 圈子里有稳定需求。 落地动作很直接:打开 MT5 的 MQL5 编辑器,把文中滚动条类挂进你已有的库工程,编译后拖一个测试面板看滚动响应;外汇与贵金属图表上叠加自绘控件存在重绘开销,实盘前请在模拟盘验证性能。

◍ 从水平滚动条派生垂直控件

上一节基于库图形元素做了水平滚动条:当子对象在左、右或两侧越出父窗体边界时,滚动条自动挂在对象上。这一节直接复用该对象,改成垂直版本——只要所连对象在顶部、底部或两端超出表格边界,表格上就生成垂直滚动条。 转换本身极轻量,基本是复制水平逻辑后调换坐标轴,所以本节更像概述。真正卡住发布的是一个老 bug:垂直滚动条早年就写好了,但图形交互时出现不可见对象部分不断“闪烁”。 根因是对象被过早、无控制地更新——先整块渲染到图表,再被裁剪回父窗体尺寸,这一先一后便闪。解法不复杂,在重绘流程里消掉过早更新即可;难的是定位那次多余重绘的调用点,耗费了不少排查时间。现已修复,后续 Windows 窗体风格控件可放心接着做。

「给 DoEasy 库补上事件定位与绘图底层方法」

做 MT5 图形面板时,常要反推某事件落在哪根虚拟 K 线的开启时间。若事件发生在烛身中间,用 iBarShift 转索引再取时间虽可行,但每次都吃 CPU。下面这个函数用纯计算直接算出开启时间,前提是事件确实落在真实柱内,速度明显优于标准函数调用。 该函数对 timeframe 做了 PERIOD_CURRENT 的就地替换,并对小于月线的周期用位运算拆出基础秒数。注释里写明算法正确有效期只到 2100-02-28,且明确不是 iBarShift 的替代品——它不依赖柱历史,只按时间整除推算。开 MT5 把代码丢进 DELib.mqh 末尾即可验证。 图形对象重着色也有坑:标准色不够用时,比如中性灰要随状态微微泛红或泛绿,只需改 RGB 单通道饱和度。GCnvElement 里新增的方法把传入分量加到原值上,超 255 就钳到 255,再用 CColor::RGBToColor 回吐颜色,避免了硬套标准色板。 另一个隐蔽 bug 是 EraseNoCrop() 不管重绘标志都刷新对象,导致被裁剪的隐藏区‘闪’一下。改成仅当标志为 true 才更新后,先清色、再 Crop() 裁剪、最后按标志重绘,图形元素后续开发才顺。 BaseObj 里直接包一层创建趋势线/箭头的方法,比先拿对象指针再绕到图形控制器快得多;ScrollBarVertical 则由水平类改 left/right 为 top/bottom 计算得来,容器越界时双滚动条才出得来。

MQL5 / C++
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class=class="str">"cmt">//| Get the opening time of the class="kw">virtual bar based on input time and                 |
class=class="str">"cmt">//| timeframe, regardless of the existence of a real bar.                           |
class=class="str">"cmt">//| It counts correctly only till class="num">28.02.class="num">2100                                        |
class=class="str">"cmt">//| It is not a replacement for iBarShift!!! It does not depend on the bar history. |
class=class="str">"cmt">//| [MQL5官方文档]                   |
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class="type">class="kw">datetime GetStartTimeOfBarFast(const ENUM_TIMEFRAMES timeframe, const class="type">class="kw">datetime time)
  {
   ENUM_TIMEFRAMES tf=(timeframe==PERIOD_CURRENT ? _Period : timeframe);
   class="type">int ts=class="num">0;
   if(tf<PERIOD_MN1)
     {
      class="type">class="kw">ushort i_tf=class="type">class="kw">ushort(tf);
      class="type">uchar _i=class="type">uchar(i_tf>>class="num">14);
      class="type">int n=i_tf & 0x0FFF;

对齐周期边界的取整逻辑

在自定义时间周期处理里,低于周线(PERIOD_W1)的图表直接靠取模把时间戳对齐到周期起点:time - time % ts,其中 ts 由传入参数 _i 决定,0 对应 n 分钟、1 对应 n 小时、其余默认按 60*60*24*7 即整周秒数处理。 周线要单独偏移,因为 MT5 的周起点并非自然周日零点的 Unix 时间;代码用 time + 4*24*60*60 把周四起点折算进取模,使返回值是该周实际开盘时刻。月线(MN1)则走一套基于 1461 天(4 年含闰)循环的日期推算,静态数组 dm[12] 存了每月相对 1 月 1 日的天数偏移,最大 337 天对应 12 月。 下面这段是月线边界对齐的核心,注意 last_days / last_result 做了静态缓存,避免同根日内重复计算;若 d 落到 276 会硬判为 9 月,这是针对闰年日序的补丁。

MQL5 / C++
class="kw">static class="type">int dm[class="num">12] = {class="num">0,class="num">31,class="num">61,class="num">92,class="num">122,class="num">153,class="num">184,class="num">214,class="num">245,class="num">275,class="num">306,class="num">337};
class="kw">static class="type">int last_days = class="num">0;
class="kw">static class="type">class="kw">datetime last_result = class="num">0;
class="type">int days = class="type">int(time/(class="num">24*class="num">60*class="num">60));
if(last_days!=days)
  {
   last_days = days;
   class="type">int d1 = (days+class="num">306+class="num">365)%class="num">1461;
   class="type">int y = d1/class="num">365;
   class="type">class="kw">datetime t1 = time - time % (class="num">24*class="num">60*class="num">60) - d1*class="num">24*class="num">60*class="num">60;
   class="type">int m = class="num">0;
   if(d1==class="num">1460)
     {
      m=class="num">11;
      y--;
     };
   class="type">int d = d1-y*class="num">365+class="num">1;
   if(d!=class="num">31)
     if(d==class="num">276)
       m = class="num">9;
     else
       m = class="type">int(d/class="num">30.68);
   if(m<class="num">0 || m>class="num">11)
     class="kw">return WRONG_VALUE;
   last_result = t1+y*class="num">365*class="num">24*class="num">60*class="num">60+dm[m]*class="num">24*class="num">60*class="num">60;
  }
class="kw">return last_result;
顺带一提,同文件里还有两组轻量工具函数声明:ChangeColorLightness 分别接收 uint 型 ARGB 与 color 型,按 change_value 调整明度;另有对应饱和度的重载未列全,但调用前确认参数范围能少踩坑。外汇与贵金属行情受杠杆影响大,这类底层时间对齐若出错,回测与实盘信号可能偏移数根 K 线,高风险须自测。

MQL5 / C++
class="kw">static class="type">int dm[class="num">12] = {class="num">0,class="num">31,class="num">61,class="num">92,class="num">122,class="num">153,class="num">184,class="num">214,class="num">245,class="num">275,class="num">306,class="num">337};
class="kw">static class="type">int last_days = class="num">0;
class="kw">static class="type">class="kw">datetime last_result = class="num">0;
class="type">int days = class="type">int(time/(class="num">24*class="num">60*class="num">60));
if(last_days!=days)
  {
   last_days = days;
   class="type">int d1 = (days+class="num">306+class="num">365)%class="num">1461;
   class="type">int y = d1/class="num">365;
   class="type">class="kw">datetime t1 = time - time % (class="num">24*class="num">60*class="num">60) - d1*class="num">24*class="num">60*class="num">60;
   class="type">int m = class="num">0;
   if(d1==class="num">1460)
     {
      m=class="num">11;
      y--;
     };
   class="type">int d = d1-y*class="num">365+class="num">1;
   if(d!=class="num">31)
     if(d==class="num">276)
       m = class="num">9;
     else
       m = class="type">int(d/class="num">30.68);
   if(m<class="num">0 || m>class="num">11)
     class="kw">return WRONG_VALUE;
   last_result = t1+y*class="num">365*class="num">24*class="num">60*class="num">60+dm[m]*class="num">24*class="num">60*class="num">60;
  }
class="kw">return last_result;

◍ 改 RGB 分量与截可见区的底层写法

做自定义指标面板时,经常要动态改某像素颜色的 RGB 分量而不破坏原有透明度。下面这个函数直接拿当前色做加法,再各自封顶到 255,适合做高亮或衰减特效。

MQL5 / C++
class="type">class="kw">color CGCnvElement::ChangeRGBComponents(class="type">class="kw">color clr,const class="type">uchar R,const class="type">uchar G,const class="type">uchar B)
  {
   class="type">class="kw">double r=CColors::GetR(clr)+R;
   if(r>class="num">255)
      r=class="num">255;
   class="type">class="kw">double g=CColors::GetG(clr)+G;
   if(g>class="num">255)
      g=class="num">255;
   class="type">class="kw">double b=CColors::GetB(clr)+B;
   if(b>class="num">255)
      b=class="num">255;
   class="kw">return CColors::RGBToColor(r,g,b);
  }
逐行拆解:第1行定义成员函数,入参为原色 clr 与三个 uchar 增量;2-3行取原 R 分量加增量,若超 255 则钳到 255;4-7行对 G、B 做同样处理;最后用 RGBToColor 合回 color 类型返回。注意增量只加不减,要变暗得传负值且原函数未做下界保护,实际用可能需自己补 <0 判断。 可见区裁剪有俩重载:一个写死只改属性(only_prop=false),另一个多一个 bool 开关。
MQL5 / C++
class="type">void SetVisibleArea(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h)
  {
   this.SetVisibleAreaX(x,false);
   this.SetVisibleAreaY(y,false);
   this.SetVisibleAreaWidth(w,false);
   this.SetVisibleAreaHeight(h,false);
  }
class="type">void SetVisibleArea(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h,const class="type">bool only_prop)
  {
   this.SetVisibleAreaX(x,only_prop);
前者四参版本把 x/y/w/h 全交给 false 分支,即立即重绘;后者第五参 only_prop 为 true 时只改属性不触发绘制,适合批量改完再统一刷新,能少几次 GPU 上传。外汇与贵金属图表叠加自绘层属高风险操作,参数越界可能闪崩图表,建议在 MT5 策略测试器先跑空帧验证。

MQL5 / C++
class="type">class="kw">color CGCnvElement::ChangeRGBComponents(class="type">class="kw">color clr,const class="type">uchar R,const class="type">uchar G,const class="type">uchar B)
  {
   class="type">class="kw">double r=CColors::GetR(clr)+R;
   if(r>class="num">255)
      r=class="num">255;
   class="type">class="kw">double g=CColors::GetG(clr)+G;
   if(g>class="num">255)
      g=class="num">255;
   class="type">class="kw">double b=CColors::GetB(clr)+B;
   if(b>class="num">255)
      b=class="num">255;
   class="kw">return CColors::RGBToColor(r,g,b);
  }

class="type">void SetVisibleArea(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h)
  {
   this.SetVisibleAreaX(x,false);
   this.SetVisibleAreaY(y,false);
   this.SetVisibleAreaWidth(w,false);
   this.SetVisibleAreaHeight(h,false);
  }
class="type">void SetVisibleArea(const class="type">int x,const class="type">int y,const class="type">int w,const class="type">int h,const class="type">bool only_prop)
  {
   this.SetVisibleAreaX(x,only_prop);

「可视区复位与擦除调用的两种写法」

在画布元素类里,ResetVisibleArea 把可见区直接重置为整个对象范围:起点 (0,0),宽高取 Width() 与 Height(),最后一个参数传 false 表示不只改属性而做实际重设。 EraseNoCrop 提供两种擦除封装。第一版无论是否重绘都调用 this.Update(redraw);第二版加了 if(redraw) 判断,只有 redraw 为真才更新图表。两种写法功能接近,但第二版在批量静默擦除时能少一次重绘开销。 进度条高光控制上,若 glare 处于 NORMAL(隐藏)态,就切到 WAITING_FADE_IN,用 ShowDelay() 设等待毫秒并记 Begin 时间。当 glare.RightEdge() >= 进度条 CoordX() 时,先 Hide() 再 Move 到进度条右缘之外,避免高光残影压住左边界。 外汇与贵金属图表叠加这类自绘控件时,频繁 Update 会拖累 MT5 主线程,建议默认 redraw=false 后由定时器统一刷新,可降低卡顿概率。

MQL5 / C++
this.SetVisibleAreaY(y,only_prop);
this.SetVisibleAreaWidth(w,only_prop);
this.SetVisibleAreaHeight(h,only_prop);
}
class=class="str">"cmt">//--- Sets the size of the visible area equal to the entire object
class="type">void ResetVisibleArea(class="type">void) { this.SetVisibleArea(class="num">0,class="num">0,this.Width(),this.Height(),false); }
class="type">void CGCnvElement::EraseNoCrop(const class="type">class="kw">color colour,const class="type">uchar opacity,const class="type">bool redraw=false)
  {
   class="type">class="kw">color arr[class="num">1];
   arr[class="num">0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   this.Update(redraw);
  }
class="type">void CGCnvElement::EraseNoCrop(const class="type">class="kw">color colour,const class="type">uchar opacity,const class="type">bool redraw=false)
  {
   class="type">class="kw">color arr[class="num">1];
   arr[class="num">0]=colour;
   this.SaveColorsBG(arr);
   this.m_canvas.Erase(::ColorToARGB(colour,opacity));
   if(redraw)
     this.Update(redraw);
  }
if(glare.DisplayState()==CANV_ELEMENT_DISPLAY_STATE_NORMAL)
  {
   glare.SetDisplayState(CANV_ELEMENT_DISPLAY_STATE_WAITING_FADE_IN);
   this.m_pause.SetWaitingMSC(this.ShowDelay());
   this.m_pause.SetTimeBegin();
   if(glare.RightEdge()>=this.CoordX())
     {
      glare.Hide();
      if(glare.Move(this.CoordX()-glare.Width(),this.CoordY()))

常见问题

先派生成一个垂直控件类,复用水平滚动条的事件定位与绘图方法,再补上可视区复位和擦除调用两种写法即可。
用对齐周期边界的取整函数把坐标 snap 到边界,避免亚像素偏移导致绘制错位。
可以,小布能加载控件脚本并替你跑通事件响应与可视区擦除,你只看交互是否符合预期。
改分量后必须调用截可见区底层写法重算裁剪范围,否则滑块会溢出父容器边界。
静态面板用直接擦除,动态频繁刷新用复位调用,减少重绘次数更稳。