DoEasy.控件(第 33 部分):垂直滚动条(基础篇)
给库加上垂直滚动条控件
在 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 计算得来,容器越界时双滚动条才出得来。
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 月,这是针对闰年日序的补丁。
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;
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,适合做高亮或衰减特效。
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);
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 后由定时器统一刷新,可降低卡顿概率。
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()))