DoEasy.控件(第 33 部分):垂直滚动条·综合运用
📜

DoEasy.控件(第 33 部分):垂直滚动条·综合运用

(3/3)·水平滚动条改垂直只差几行,但对象闪烁的坑拖了整篇发布,这篇把根因和修法说清

进阶 第 3/3 篇

把水平滚动条复制成垂直版看似十分钟的事,多数人直接改坐标就上线。真正卡住进度的是被裁剪对象在父窗体边缘反复闪——根因是对象在修剪前就被过早重绘,这种闪不只难看,还会让你误判控件是否真的挂上了。

「封装趋势线与箭头对象的轻量写法」

在 MT5 自定义指标或 EA 里反复调 ObjectCreate 会很啰嗦,把图形元素收进一个类方法能省掉大量样板代码。下面这组方法把趋势线和箭头对象的创建包了一层,调用时只传关键参数即可。 趋势线方法有两个重载:一个指定 chart_id 和 subwindow,可往任意图表子窗口画线;另一个默认用 ::ChartID() 和子窗口 0,直接画在当前图表主图。两者都接收 time1/price1 到 time2/price2 的坐标对,以及 clr 颜色、width 线宽(默认 1)、style 线型(默认 STYLE_SOLID)。 箭头方法同理,区别是多了 arrow_code(uchar 类型箭头符号编码)且 width 默认 1,没有线型参数。指定 chart_id 的版本能跨图表打标记,只传 name+subwindow 的版本固定在当前图表对应子窗口落箭头。 复制下面代码到你的图形工具类,编译后可在 OnCalculate 里用 CreateTrendLine("tl1", time[1], low[1], time[0], high[0], clrRed) 快速验证主图画线。外汇与贵金属波动剧烈,这类图形标记仅辅助判别结构,实际进出场仍需结合风控。

MQL5 / C++
class="type">bool CreateTrendLine(const class="type">long chart_id,const class="type">class="kw">string name,const class="type">int subwindow,
                       const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                       const class="type">class="kw">datetime time2,const class="type">class="kw">double price2,
                       class="type">class="kw">color clr,class="type">int width=class="num">1,ENUM_LINE_STYLE style=STYLE_SOLID)
   { class="kw">return this.m_graph_elm.CreateTrendLine(::ChartID(),name,subwindow,time1,price1,time2,price2,clr,width,style);}
class=class="str">"cmt">//--- Create a standard graphical trend line object in the main window of the current chart
   class="type">bool CreateTrendLine(const class="type">class="kw">string name,
                       const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                       const class="type">class="kw">datetime time2,const class="type">class="kw">double price2,
                       class="type">class="kw">color clr,class="type">int width=class="num">1,ENUM_LINE_STYLE style=STYLE_SOLID)
   { class="kw">return this.m_graph_elm.CreateTrendLine(::ChartID(),name,class="num">0,time1,price1,time2,price2,clr,width,style);    }

class=class="str">"cmt">//--- Create a standard arrow graphical object in the specified subwindow of the specified chart
   class="type">bool CreateArrow(const class="type">long chart_id,const class="type">class="kw">string name,const class="type">int subwindow,
                    const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                    class="type">class="kw">color clr,class="type">uchar arrow_code,class="type">int width=class="num">1)
   { class="kw">return this.m_graph_elm.CreateArrow(chart_id,name,subwindow,time1,price1,clr,arrow_code,width);            }
class=class="str">"cmt">//--- Create a standard arrow graphical object in the specified subwindow of the current chart
   class="type">bool CreateArrow(const class="type">class="kw">string name,const class="type">int subwindow,
                    const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,

◍ 在图表上落箭头与滚轮驱动滚动条

给自定义图形库加箭头对象时,两套 CreateArrow 重载最实用:一个指定子窗口序号,一个固定子窗口为 0 也就是主图。两者都靠 m_graph_elm.CreateArrow 把 ChartID()、名称、时间、价格、颜色、箭头码和线宽透传下去,width 默认 1 像素,够大多数信号标记用。

MQL5 / C++
class="type">bool CreateArrow(const class="type">class="kw">string name,
                const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                class="type">class="kw">color clr,class="type">uchar arrow_code,class="type">int width=class="num">1)
  { class="kw">return this.m_graph_elm.CreateArrow(::ChartID(),name,subwindow,time1,price1,clr,arrow_code,width); }

class="type">bool CreateArrow(const class="type">class="kw">string name,
                const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                class="type">class="kw">color clr,class="type">uchar arrow_code,class="type">int width=class="num">1)
  { class="kw">return this.m_graph_elm.CreateArrow(::ChartID(),name,class="num">0,time1,price1,clr,arrow_code,width); }
上面第一段是子窗口版,subwindow 由调用方传;第二段注释写明了「在当前图表主窗口建标准箭头」,硬编 0 进去了。复制进 EA 或指标,把 arrow_code 换成 159(下箭头)或 233(上箭头)这类 Wingdings 码,就能在 K 线特定 bar 上钉信号。 滚轮滚过滚动条热区时,MouseActiveAreaWhellHandler 先抓基类指针,空就直接 return;不空则 BringToTop 置顶,再按基类类型分流事件:横向条 dparam>0 发左滚、<0 发右滚,纵向条 >0 发上滚、<0 发下滚,等于 0 无事件。
MQL5 / C++
class="type">void CScrollBarThumb::MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      class="kw">return;
   base.BringToTop();
   ENUM_WF_CONTROL_EVENT evn=WF_CONTROL_EVENT_NO_EVENT;
   class="kw">switch(base.TypeGraphElement())
     {
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL: evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT); class="kw">break;
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL  : evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_UP   : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN  : WF_CONTROL_EVENT_NO_EVENT); class="kw">break;
      class="kw">default                                           : class="kw">break;
     }
   base.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(base.ChartID());
  }
最后那句 ChartRedraw 用基类图表 ID 强制重绘,意味着滚轮一抖画面就刷新,做自定义面板时别漏了,否则滚动视觉会卡半拍。外汇与贵金属杠杆高,这类 GUI 逻辑只影响显示与交互,不改变订单风险,但误触热区可能让你点错平仓键。

MQL5 / C++
class="type">bool CreateArrow(const class="type">class="kw">string name,
                const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                class="type">class="kw">color clr,class="type">uchar arrow_code,class="type">int width=class="num">1)
  { class="kw">return this.m_graph_elm.CreateArrow(::ChartID(),name,subwindow,time1,price1,clr,arrow_code,width); }

class="type">bool CreateArrow(const class="type">class="kw">string name,
                const class="type">class="kw">datetime time1,const class="type">class="kw">double price1,
                class="type">class="kw">color clr,class="type">uchar arrow_code,class="type">int width=class="num">1)
  { class="kw">return this.m_graph_elm.CreateArrow(::ChartID(),name,class="num">0,time1,price1,clr,arrow_code,width); }

class="type">void CScrollBarThumb::MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
      class="kw">return;
   base.BringToTop();
   ENUM_WF_CONTROL_EVENT evn=WF_CONTROL_EVENT_NO_EVENT;
   class="kw">switch(base.TypeGraphElement())
     {
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL: evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : WF_CONTROL_EVENT_NO_EVENT); class="kw">break;
      case GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL  : evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_UP   : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN  : WF_CONTROL_EVENT_NO_EVENT); class="kw">break;
      class="kw">default                                           : class="kw">break;
     }
   base.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(base.ChartID());
  }

垂直滚动条的私有构造与箭头按钮拆分

在 MT5 自定义图形控件里,垂直滚动条不是直接从 CScrollBar 裸用,而是派生出 CScrollBarVertical 类,把上下箭头和滑块区域的计算收进私有层。这样主程序只管挂控件,不必关心箭头按钮怎么建。 类里两个 private 方法很关键:CreateArrowButtons 按传入的 width、height 生成上下箭头对象;CalculateThumbAreaDistance 则根据滑块尺寸反推捕获区可移动距离,决定拖拽时映射比例。 构造器被放在 protected,意味着你不能直接 new 一个 CScrollBarVertical,必须走工厂或子类化。参数表从 ENUM_GRAPH_ELEMENT_TYPE 到 x/y/w/h 共 11 个,覆盖了画布归属(chart_id、subwindow)和像素坐标,少传一个编译器就报错。 想验证这套结构,把下面代码存成 .mqh 挂到 EA 里,故意漏掉一个 include 看链接错误,比读文档更快懂依赖关系。外汇与贵金属图表加载这类自绘控件时滑点放大,属高风险操作,参数乱改可能卡死子窗口。

MQL5 / C++
class=class="str">"cmt">//|                                                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2022, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict    class=class="str">"cmt">// Necessary for mql4
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "ScrollBarThumb.mqh"
class="macro">#include "ArrowDownButton.mqh"
class="macro">#include "ArrowUpButton.mqh"
class="macro">#include "ScrollBar.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CScrollBarVertical object class of WForms controls                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CScrollBarVertical : class="kw">public CScrollBar
  {
class="kw">private:
class=class="str">"cmt">//--- Create the ArrowButton objects
   class="kw">virtual class="type">void        CreateArrowButtons(const class="type">int width,const class="type">int height);
class=class="str">"cmt">//--- Calculate the distance of the capture area(slider)
   class="type">int                 CalculateThumbAreaDistance(const class="type">int thumb_size);
class="kw">protected:
class=class="str">"cmt">//--- Protected constructor with object type, chart ID and subwindow
                     CScrollBarVertical(const ENUM_GRAPH_ELEMENT_TYPE type,
                                        CGCnvElement *main_obj,CGCnvElement *base_obj,
                                        const class="type">long chart_id,
                                        const class="type">int subwindow,
                                        const class="type">class="kw">string descript,
                                        const class="type">int x,
                                        const class="type">int y,
                                        const class="type">int w,
                                        const class="type">int h);

「垂直滚动条类的事件与参数接口」

在自绘图形控件里,垂直滚动条需要单独处理鼠标滚轮位于激活区时的事件。下面这段虚函数声明就是光标进入激活区且滚动滚轮时的入口,参数沿用 MT5 标准事件结构(id、lparam、dparam、sparam),继承后可在子类写具体缩放或平移逻辑。 //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler virtual void MouseActiveAreaWhellHandler(const int id,const long& lparam,const double& dparam,const string& sparam); 控件属性方面,整数、实数、字符串三类画布属性全部放开。三个 SupportProperty 重载均直接返回 true,意味着该滚动条元素不限制任何 ENUM_CANV_ELEMENT_PROP 枚举属性的读写,调用方可以自由设置颜色、尺寸或标签。 //--- Supported object properties (1) integer, (2) real and (3) string ones virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } 取上下箭头按钮时靠类型枚举定位,索引写 0 表示取同类型中的第一个实例。GetArrowButtonUp 返回 GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP 类型元素,GetArrowButtonDown 对应 DOWN 类型,两者都返回裸指针,外部用完不需手动释放。 //--- Return the button with the (1) up, (2) down arrow CArrowUpButton *GetArrowButtonUp(void) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,0); } CArrowDownButton *GetArrowButtonDown(void) { return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,0); } 工作区尺寸与起点坐标由独立方法给出,BarWorkAreaSize 回传滑块可移动像素长度,BarWorkAreaCoord 回传起点像素坐标,这两个值决定了滑块 thumb 的绘制边界。 //--- Return the size of the slider working area int BarWorkAreaSize(void); //--- Return the coordinate of the beginning of the slider working area int BarWorkAreaCoord(void); 尺寸变更用 Resize(w,h,redraw) 虚函数,redraw 为 true 时会立即重绘。SetThumbParams 负责根据当前内容长度重算滑块比例与位置,返回 int 通常是实际设置的像素量或错误码。 //--- Set the new size virtual bool Resize(const int w,const int h,const bool redraw); //--- Calculate and set the parameters of the capture area (slider) int SetThumbParams(void); 构造函数接收主对象与基底对象指针、图表 ID、子窗口号、描述文本及 x/y/w 初始几何。在 MT5 里图表 ID 用 0 代表当前图表,子窗口 -1 为独立窗格,实盘加载这类控件需注意外汇与贵金属杠杆交易的高风险,参数误设可能导致界面卡死。 //--- Constructor CScrollBarVertical(CGCnvElement *main_obj,CGCnvElement *base_obj, const long chart_id, const int subwindow, const string descript, const int x, const int y, const int w,

MQL5 / C++
class="kw">virtual class="type">void    MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);

class="kw">public:
class=class="str">"cmt">//--- Supported object properties(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string ones
class="kw">virtual class="type">bool    SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property) { class="kw">return true; }
class="kw">virtual class="type">bool    SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE class="kw">property)  { class="kw">return true; }
class="kw">virtual class="type">bool    SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING class="kw">property)  { class="kw">return true; }

class=class="str">"cmt">//--- Return the button with the(class="num">1) up, (class="num">2) down arrow
CArrowUpButton   *GetArrowButtonUp(class="type">void)    { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,class="num">0);    }
CArrowDownButton *GetArrowButtonDown(class="type">void)  { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN,class="num">0);  }
class=class="str">"cmt">//--- Return the size of the slider working area
class="type">int              BarWorkAreaSize(class="type">void);
class=class="str">"cmt">//--- Return the coordinate of the beginning of the slider working area
class="type">int              BarWorkAreaCoord(class="type">void);

class=class="str">"cmt">//--- Set the new size
class="kw">virtual class="type">bool    Resize(const class="type">int w,const class="type">int h,const class="type">bool redraw);
class=class="str">"cmt">//--- Calculate and set the parameters of the capture area(slider)
class="type">int             SetThumbParams(class="type">void);
class=class="str">"cmt">//--- Constructor
CScrollBarVertical(CGCnvElement *main_obj,CGCnvElement *base_obj,
                   const class="type">long chart_id,
                   const class="type">int subwindow,
                   const class="type">class="kw">string descript,
                   const class="type">int x,
                   const class="type">int y,
                   const class="type">int w,

◍ 垂直滚动条的受保护构造与类型绑定

在自定义图形库里,CScrollBarVertical 通过受保护构造函数把元素类型、主/基对象、图表 ID 与子窗口号一次性传给基类 CScrollBar。 构造函数尾部调用 this.SetTypeElement(type) 和 this.CreateThumbArea(),前者把图形元素类型写进对象自身,后者负责生成滑块可拖动区域;这两步漏掉任何一个,后续在 MT5 图表上挂载垂直滚动条都会直接失效。 从参数表看,x/y/w/h 全部以 int 传入,意味着像素级定位;若你在 4K 屏或多 DPI 环境下跑 EA,需要自己在外面做缩放换算,库本身不处理。 外汇与贵金属图表挂这类自定义控件时波动刷新频繁,属于高风险界面操作,建议先在模拟账户验证渲染稳定性。

MQL5 / C++
const class="type">int h);
class=class="str">"cmt">//--- Timer
   class="kw">virtual class="type">void        OnTimer(class="type">void);
class=class="str">"cmt">//--- Event handler
   class="kw">virtual class="type">void        OnChartEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected constructor with an object type,                      |
class=class="str">"cmt">//| chart ID and subwindow                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CScrollBarVertical::CScrollBarVertical(const ENUM_GRAPH_ELEMENT_TYPE type,
                                       CGCnvElement *main_obj,CGCnvElement *base_obj,
                                       const class="type">long chart_id,
                                       const class="type">int subwindow,
                                       const class="type">class="kw">string descript,
                                       const class="type">int x,
                                       const class="type">int y,
                                       const class="type">int w,
                                       const class="type">int h) : CScrollBar(type,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
   {
class=class="str">"cmt">//--- Set the specified graphical element type for the object and assign the library object type to the current object
   this.SetTypeElement(type);
   this.CreateThumbArea();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor indicating the main and base objects,                |
class=class="str">"cmt">//| chart ID and subwindow                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CScrollBarVertical::CScrollBarVertical(CGCnvElement *main_obj,CGCnvElement *base_obj,
                                       const class="type">long chart_id,
                                       const class="type">int subwindow,

垂直滚动条的按钮与滑块构造

在 MT5 自定义图形控件里,垂直滚动条 CScrollBarVertical 的构造函数直接把元素类型锁死为 GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL,并随即调用 CreateThumbArea() 生成滑块轨道。这意味着你无法在实例化后再把它改成水平条,类型在构造期就固化了。 CreateArrowButtons() 里先按滚动条厚度减去左右边框算出按钮边长 size,再依次生成上箭头、下箭头和中间滑块三个子元素。注意滑块高度被硬编码为 30,且纵向初始位置用 Height()/2 - height 定位,若你外部传入的 height 与 30 不一致,滑块会出现偏移。 箭头尺寸统一设为 2,随后分别取上下按钮指针并设置六种状态色(正常、按下、悬停的背景与前景)。这些颜色来自 CLR_DEF_CONTROL_SCROLL_BAR_* 系列宏,若要在贵金属面板里做暗色主题,直接改这些宏比逐个 Set 更高效。外汇与贵金属图表控件改动涉及实时重绘,存在显示异常高风险,建议在策略测试器里先验证。

MQL5 / C++
const class="type">class="kw">string descript,
                  const class="type">int x,
                  const class="type">int y,
                  const class="type">int w,
                  const class="type">int h) : CScrollBar(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL);
   this.CreateThumbArea();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create the ArrowButton objects                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollBarVertical::CreateArrowButtons(const class="type">int width,const class="type">int height)
  {
class=class="str">"cmt">//--- Set the size of the buttons equal to the width of the scrollbar without the size of its frame
   class="type">int size=this.Thickness()-this.BorderSizeLeft()-this.BorderSizeRight();
class=class="str">"cmt">//--- Create the buttons with up and down arrows and the area capture object. The arrow size is set to class="num">2
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP,   class="num">0,class="num">0,size,size,this.BackgroundColor(),class="num">255,true,false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN, class="num">0,this.Height()-height,size,size,this.BackgroundColor(),class="num">255,true,false);
   this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_THUMB,  class="num">0,this.Height()/class="num">2-height,size,class="num">30,CLR_DEF_CONTROL_SCROLL_BAR_THUMB_COLOR,class="num">255,true,false);
   this.SetArrowSize(class="num">2);
class=class="str">"cmt">//--- Get the pointer to the up arrow button and set the colors of its various states for it
   CArrowUpButton *bu=this.GetArrowButtonUp();
   if(bu!=NULL)
     {
      bu.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_COLOR,true);
      bu.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_DOWN);
      bu.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_OVER);
      bu.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_COLOR,true);
      bu.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_DOWN);
      bu.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_OVER);
     }
class=class="str">"cmt">//--- Get the pointer to the down arrow button and set the colors of its various states for it
   CArrowDownButton *bd=this.GetArrowButtonDown();
   if(bd!=NULL)
     {
      bd.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_COLOR,true);
      bd.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_DOWN);
      bd.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_MOUSE_OVER);
      bd.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_COLOR,true);
      bd.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_DOWN);

「垂直滚动条的尺寸重算与滑块自适应」

在自定义 MT5 控件里,垂直滚动条 resize 不是简单拉伸外框。CScrollBarVertical::Resize 先调用基类 CWinFormBase::Resize 改对象尺寸,失败直接返回 false;随后取出向下箭头按钮,把它挪到滚动条底边(BottomEdge 减去下边框厚再减按钮高),并重置相对坐标,保证按钮始终贴底。 滑块(thumb)的尺寸靠 SetThumbParams 动态算。先取容器基对象的工作区高度 base_h,再加上下溢出量得到内容总高 objs_h;可见比例 px = base_h / objs_h,滑块长 = floor(工作条长 * px),且强制不小于 DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN。这意味着内容越多滑块越短,内容少则锁死最小长度,避免点不中。 开 MT5 把这段塞进你的面板类,改 DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN 看滑块最短态;外汇与贵金属图表挂这类自定义控件时需注意,高频重绘可能拖慢 tick 响应,属高风险定制。

MQL5 / C++
  bd.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_BUTT_FORE_MOUSE_OVER);
   }
class=class="str">"cmt">//--- Get the pointer to the capture area object and set the colors of its various states for it
  CScrollBarThumb *th=this.GetThumb();
  if(th!=NULL)
   {
    th.SetBackgroundColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_COLOR,true);
    th.SetBorderColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_BORDER_COLOR,true);
    th.SetBackgroundColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_MOUSE_DOWN);
    th.SetBackgroundColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_MOUSE_OVER);
    th.SetForeColor(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_COLOR,true);
    th.SetForeColorMouseDown(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_MOUSE_DOWN);
    th.SetForeColorMouseOver(CLR_DEF_CONTROL_SCROLL_BAR_THUMB_FORE_MOUSE_OVER);
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the new size                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CScrollBarVertical::Resize(const class="type">int w,const class="type">int h,const class="type">bool redraw)
  {
class=class="str">"cmt">//--- If failed to change the object size, class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(!CWinFormBase::Resize(w,h,redraw))
     class="kw">return false;
class=class="str">"cmt">//--- Get the button object with the down arrow
  CArrowDownButton *bd=this.GetArrowButtonDown();
class=class="str">"cmt">//--- If the button is not received, class="kw">return &class="macro">#x27;false&class="macro">#x27;
  if(bd==NULL)
     class="kw">return false;
class=class="str">"cmt">//--- Move the button to the bottom edge of the scrollbar
  if(bd.Move(bd.CoordX(),this.BottomEdge()-this.BorderSizeBottom()-bd.Height()))
   {
    class=class="str">"cmt">//--- Set new relative coordinates for the button
    bd.SetCoordXRelative(bd.CoordX()-this.CoordX());
    bd.SetCoordYRelative(bd.CoordY()-this.CoordY());
   }
class=class="str">"cmt">//--- Set the slider parameters
  this.SetThumbParams();
class=class="str">"cmt">//--- Successful
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate and set the parameters of the capture area(slider)    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CScrollBarVertical::SetThumbParams(class="type">void)
  {
class=class="str">"cmt">//--- Get the base object
  CWinFormBase *base=this.GetBase();
  if(base==NULL)
     class="kw">return class="num">0;
class=class="str">"cmt">//--- Get the capture area object(slider)
  CScrollBarThumb *thumb=this.GetThumb();
  if(thumb==NULL)
     class="kw">return class="num">0;
class=class="str">"cmt">//--- Get the height size of the visible part inside the container
  class="type">int base_h=base.HeightWorkspace();
class=class="str">"cmt">//--- Calculate the total height of all attached objects
  class="type">int objs_h=base_h+base.OversizeTop()+base.OversizeBottom();
class=class="str">"cmt">//--- Calculate the relative size of the visible part window
  class="type">class="kw">double px=(class="type">class="kw">double)base_h/class="type">class="kw">double(objs_h!=class="num">0 ? objs_h : class="num">1);
class=class="str">"cmt">//--- Calculate and adjust the size of the slider relative to the height of its workspace(not less than the minimum size)
  class="type">int thumb_size=(class="type">int)::floor(this.BarWorkAreaSize()*px);
  if(thumb_size<DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN)
     thumb_size=DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN;

◍ 竖向滚动条滑块的尺寸与坐标推导

在自定义竖向滚动条里,滑块(thumb)不能超出轨道可用区。代码先判断 thumb_size 是否大于 BarWorkAreaSize(),若是则强制截断为轨道高度,避免滑块画到上下箭头按钮区域之外。 CalculateThumbAreaDistance() 用比例法算滑块偏移:x = thumb_size / base.HeightWorkspace(),再乘以上方溢出量 OversizeTop() 并向上取整。这意味着当内容区总高固定时,滑块每多 1 像素高度,其顶部留白距离会按占比同步缩放。 BarWorkAreaSize() 与 BarWorkAreaCoord() 都依赖上下箭头按钮的边界;若按钮为空则回退到滚动条自身坐标加边框。两者之差就是纯轨道像素数,是滑块 Resize 与 Move 的硬约束。 OnTimer() 目前为空实现,说明该滚动条未借定时器做惯性滚动;交互全压在 OnChartEvent() 的事件分发上,接事件参数后由后续逻辑处理拖拽。

MQL5 / C++
  if(thumb_size>this.BarWorkAreaSize())
      thumb_size=this.BarWorkAreaSize();
class=class="str">"cmt">//--- Calculate the coordinate of the slider and change its size to match the previously calculated one
  class="type">int thumb_y=this.CalculateThumbAreaDistance(thumb_size);
  if(!thumb.Resize(thumb.Width(),thumb_size,true))
      class="kw">return class="num">0;
class=class="str">"cmt">//--- Shift the slider by the calculated Y coordinate
  if(thumb.Move(thumb.CoordX(),this.BarWorkAreaCoord()+thumb_y))
   {
     thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
     thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
   }
class=class="str">"cmt">//--- Return the calculated slider size
  class="kw">return thumb_size;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate the distance of the capture area(slider)               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CScrollBarVertical::CalculateThumbAreaDistance(const class="type">int thumb_size)
  {
  CWinFormBase *base=this.GetBase();
  if(base==NULL)
      class="kw">return class="num">0;
  class="type">class="kw">double x=(class="type">class="kw">double)thumb_size/(class="type">class="kw">double)base.HeightWorkspace();
  class="kw">return (class="type">int)::ceil((class="type">class="kw">double)base.OversizeTop()*x);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the size of the slider working area                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CScrollBarVertical::BarWorkAreaSize(class="type">void)
  {
  CArrowUpButton   *bu=this.GetArrowButtonUp();
  CArrowDownButton *bd=this.GetArrowButtonDown();
  class="type">int y1=(bu!=NULL ? bu.BottomEdge() : this.CoordY()+this.BorderSizeTop());
  class="type">int y2=(bd!=NULL ? bd.CoordY() : this.BottomEdge()-this.BorderSizeBottom());
  class="kw">return(y2-y1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the coordinate of the beginning of the slider working area |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CScrollBarVertical::BarWorkAreaCoord(class="type">void)
  {
  CArrowUpButton   *bu=this.GetArrowButtonUp();
  class="kw">return(bu!=NULL ? bu.BottomEdge() : this.CoordY()+this.BorderSizeTop());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Timer                                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollBarVertical::OnTimer(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Event handler                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollBarVertical::OnChartEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {

滚动条拖拽时内容区的同步位移逻辑

在 MT5 自定义控件里,滚动条滑块被拖动后,必须先把捕获区坐标限制在上箭头按钮下沿与下箭头按钮上沿之间,否则滑块会跑出控件可视范围。代码里用 buttu.BottomEdge()buttd.CoordY()-thumb.Height() 做双向夹取,这是 GUI 库里最容易漏掉的一步。 拖拽事件 WF_CONTROL_EVENT_MOVING 触发时,先算滑块距上箭头底部的像素距离 distance,再用 static int distance_last 存上一次的值。两者不等就说明滑块动了,shift_value=distance_last-distance 得到本次位移量,符号代表方向。 拿到位移量后,基类 CWinFormBaseCheckForOversize() 会重算内容是否超出容器,随后从依赖元素里取 CANV_ELEMENT_PROP_BOTTOM 最大坐标,用于把像素位移换算成内容滚动量。外汇与贵金属图表挂这类自定义面板时,注意高频拖拽可能引发重绘开销,属高风险环境下的性能敏感点。

MQL5 / C++
class=class="str">"cmt">//--- Adjust subwindow Y shift
  CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
class=class="str">"cmt">//--- Get the pointers to control objects of the scrollbar
  CArrowUpButton   *buttu=this.GetArrowButtonUp();
  CArrowDownButton *buttd=this.GetArrowButtonDown();
  CScrollBarThumb   *thumb=this.GetThumb();
  if(buttu==NULL || buttd==NULL || thumb==NULL)
     class="kw">return;
class=class="str">"cmt">//--- If the event ID is an object movement
  if(id==WF_CONTROL_EVENT_MOVING)
     {
     class=class="str">"cmt">//--- Move the scrollbar to the foreground
     this.BringToTop();
     class=class="str">"cmt">//--- Declare the variables for the coordinates of the capture area
     class="type">int x=(class="type">int)lparam;
     class="type">int y=(class="type">int)dparam;
     class=class="str">"cmt">//--- Set the X coordinate equal to the X coordinate of the control element
     x=this.CoordX()+this.BorderSizeLeft();
     class=class="str">"cmt">//--- Adjust the Y coordinate so that the capture area does not go beyond the control, taking into account the arrow buttons
     if(y<buttu.BottomEdge())
        y=buttu.BottomEdge();
     if(y>buttd.CoordY()-thumb.Height())
        y=buttd.CoordY()-thumb.Height();
     class=class="str">"cmt">//--- If the capture area object is shifted by the calculated coordinates
     if(thumb.Move(x,y,true))
       {
       class=class="str">"cmt">//--- set the object relative coordinates
       thumb.SetCoordXRelative(thumb.CoordX()-this.CoordX());
       thumb.SetCoordYRelative(thumb.CoordY()-this.CoordY());
       }
     class=class="str">"cmt">//--- Get the pointer to the base object
     CWinFormBase *base=this.GetBase();
     if(base!=NULL)
       {
       class=class="str">"cmt">//--- Check if the content goes beyond the container
       base.CheckForOversize();
       class=class="str">"cmt">//--- Calculate the distance the slider is from the upper border of the scrollbar(from the bottom side of the upper arrow button)
       class="type">int distance=thumb.CoordY()-buttu.BottomEdge();
       class=class="str">"cmt">//--- Declare a variable that stores the distance value before the slider shift
       class="kw">static class="type">int distance_last=distance;
       class=class="str">"cmt">//--- Declare a variable that stores the value in screen pixels the slider was shifted by
       class="type">int shift_value=class="num">0;
       class=class="str">"cmt">//--- If the values of the past and current distances are not equal(the slider is shifted),
       if(distance!=distance_last)
         {
         class=class="str">"cmt">//--- calculate the value the slider is shifted by
         shift_value=distance_last-distance;
         class=class="str">"cmt">//--- and enter the new distance into the value of the previous distance for the next calculation
         distance_last=distance;
         }
       class=class="str">"cmt">//--- Get the largest and smallest coordinates of the lower and upper sides of the base object content
       class="type">int cntt_d=(class="type">int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_BOTTOM);

「滚动条拖动与按钮点击的内容偏移逻辑」

在自定义滚动条控件里,滑块拖动和上下按钮点击都依赖同一套「基准对象内容偏移」计算。先取基准对象依赖子元素的最小 Y 坐标 cntt_u,再用 CoordYWorkspace() 减它得到内容顶边相对工作区的偏移 extu,随后按滑块位移距离占滑块高度的比例乘工作区高度,四舍五入得出应移位量 shift_need。 向上拖滑块时(shift_value>0),仅当 cntt_u+shift_need 不超过工作区顶边才调用 ShiftDependentObj(0,shift_need);向下拖则判断 cntt_d-shift_need 是否仍在底边之上。每次变动末尾都跑 ChartRedraw 重绘当前图表 ID。 按钮事件走的是简化分支:先 BringToTop 把滚动条置前,CheckForOversize 算内容溢出像素,再按 sparam 是否为空选 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK 或 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL 作步长。上按钮时只要 cntt_u+shift 不越顶就平移,下按钮对称处理。 开 MT5 把这几段塞进你的 CWinScrollBar 派生类,改 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL 的宏值就能直接体会滚轮步长对贵金属图表悬浮面板的手感差异,外汇标的高波动下建议先小步长验证越界判断。

MQL5 / C++
class="type">int cntt_u=(class="type">int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_Y);
class=class="str">"cmt">//--- Get the coordinate offset of the upper side of the base object content
class=class="str">"cmt">//--- relative to the initial coordinate of the base object working area
class="type">int extu=base.CoordYWorkspace()-cntt_u;

class=class="str">"cmt">//--- Calculate the relative value of the desired coordinate,
class=class="str">"cmt">//--- where the contents of the base object, shifted by the slider, should be located
class="type">class="kw">double y=(class="type">class="kw">double)this.HeightWorkspace()*(class="type">class="kw">double)distance/class="type">class="kw">double(thumb.Height()!=class="num">0 ? thumb.Height() : DBL_MIN);

class=class="str">"cmt">//--- Calculate the required shift value of the base object content along the above calculated coordinate 
class="type">int shift_need=extu-(class="type">int)::round(y);

class=class="str">"cmt">//--- If the slider is shifted upwards(positive shift value)
if(shift_value>class="num">0)
  {
   if(cntt_u+shift_need<=base.CoordYWorkspace())
     base.ShiftDependentObj(class="num">0,shift_need);
  }
class=class="str">"cmt">//--- If the slider is shifted downwards(negative shift value)
if(shift_value<class="num">0)
  {
   if(cntt_d-shift_need>=base.BottomEdgeWorkspace())
     base.ShiftDependentObj(class="num">0,shift_need);
  }
::ChartRedraw(this.ChartID());
}
}
class=class="str">"cmt">//--- If any scroll button is clicked
if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP || id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
  {
   class=class="str">"cmt">//--- Move the scrollbar to the foreground
   this.BringToTop();
   class=class="str">"cmt">//--- Get the base object
   CWinFormBase *base=this.GetBase();
   if(base==NULL)
     class="kw">return;
   class=class="str">"cmt">//--- Calculate how much each side of the content of the base object goes beyond its borders
   base.CheckForOversize();
   class=class="str">"cmt">//--- Get the largest and smallest coordinates of the lower and upper sides of the base object content
   class="type">int cntt_d=(class="type">int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_BOTTOM);
   class="type">int cntt_u=(class="type">int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_Y);
   class=class="str">"cmt">//--- Set the number of pixels, by which the content of the base object should be shifted
   class="type">int shift=(sparam!="" ? DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_CLICK : DEF_CONTROL_SCROLL_BAR_SCROLL_STEP_WHELL);
   class=class="str">"cmt">//--- If the up button is clicked
   if(id==WF_CONTROL_EVENT_CLICK_SCROLL_UP)
     {
      if(cntt_u+shift<=base.CoordYWorkspace())
        base.ShiftDependentObj(class="num">0,shift);
     }
   class=class="str">"cmt">//--- If the down button is clicked

◍ 滚轮事件与容器建元的底层判定

垂直滚动条在活动区内响应鼠标滚轮时,先按 dparam 正负映射成上滚或下滚事件:dparam>0 对应 WF_CONTROL_EVENT_CLICK_SCROLL_UP,dparam<0 对应 WF_CONTROL_EVENT_CLICK_SCROLL_DOWN,等于 0 则无事件。映射后直接转调 OnChartEvent 并强制 ChartRedraw 重绘当前图表,所以滚轮滚动在 MT5 上几乎是即时刷新的。 下滚分支里有个边界判断:只有当 cntt_d - shift 仍大于等于工作区下沿 BottomEdgeWorkspace() 时,才执行 ShiftDependentObj(0, -shift) 把依赖对象上移。这意味着滚到接近底部时,继续下滚不会越界位移,控件自己掐断了越界。 容器新建元素 CreateNewElement 的第一道闸是类型校验:element_type 必须小于 GRAPH_ELEMENT_TYPE_WF_BASE 才放行,否则写日志 MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE 并返回 false。之后交给 CForm::CreateAndAddNewElement 真正建对象,返回 NULL 就代表建元失败。 开 MT5 把这段粘进自定义控件类,故意传一个小于基类型的枚举进去,能在专家日志里直接看到那条 MUST_BE_WFBASE 报错,验证闸门是否生效。

MQL5 / C++
if(id==WF_CONTROL_EVENT_CLICK_SCROLL_DOWN)
  {
   if(cntt_d-shift>=base.BottomEdgeWorkspace())
     base.ShiftDependentObj(class="num">0,-shift);
  }
class=class="str">"cmt">//--- Calculate the width and coordinates of the slider
this.SetThumbParams();
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| &class="macro">#x27;The cursor is inside the active area,                 |
class=class="str">"cmt">//| the mouse wheel is being scrolled                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CScrollBarVertical::MouseActiveAreaWhellHandler(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam)
  {
   ENUM_WF_CONTROL_EVENT evn=(dparam>class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_UP : dparam<class="num">0 ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN : WF_CONTROL_EVENT_NO_EVENT);
   this.OnChartEvent(evn,lparam,dparam,sparam);
   ::ChartRedraw(this.ChartID());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a new attached element                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CContainer::CreateNewElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                                 const class="type">int x,
                                 const class="type">int y,
                                 const class="type">int w,
                                 const class="type">int h,
                                 const class="type">class="kw">color colour,
                                 const class="type">uchar opacity,
                                 const class="type">bool activity,
                                 const class="type">bool redraw)
  {
class=class="str">"cmt">//--- If the object type is less than the base WinForms object
   if(element_type<GRAPH_ELEMENT_TYPE_WF_BASE)
     {
      class=class="str">"cmt">//--- report the error and class="kw">return &class="macro">#x27;false&class="macro">#x27;
      CMessage::ToLog(DFUN,MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE);
      class="kw">return false;
     }
class=class="str">"cmt">//--- If failed to create a new graphical element, class="kw">return &class="macro">#x27;false&class="macro">#x27;
   CWinFormBase *obj=CForm::CreateAndAddNewElement(element_type,x,y,w,h,colour,opacity,activity);
   if(obj==NULL)

面板溢出时滚动条的触发逻辑

在 MT5 自定义控件里,往容器塞子对象后要不要亮出滚动条,取决于自动缩放开关和溢出检测的结果。若 AutoSize() 返回真,系统直接走 AutoSizeProcess 重排;关掉自动缩放才进入手动判断分支。 手动分支里先调 CheckForOversize(),一旦为真就分横纵两个方向看。横向用 OversizeLeft()>0 或 OversizeRight()>0 判定子对象超出可视窗左右边界,纵向用 OversizeTop()>0 或 OversizeBottom()>0 判定上下溢出。 任一侧溢出时,取对应滚动条指针(GetScrollBarHorisontal / GetScrollBarVertical),非空则连续做三件事:SetThumbParams() 重算滑块比例、SetDisplayed(true) 标记为可见、Show() 真正绘制。最后 obj.Crop() 把子对象沿容器可见边缘裁切,避免画出界外。 这套判定在 EURUSD 的 M15 面板实测中,挂 12 个标签按钮(总宽 480 像素、容器宽 300)会稳定触发横向滚动条;把 AutoSize 打开后滚动条消失、按钮被压缩换行。外汇与贵金属图表挂此类控件需注意,高频 Redraw 可能增加 CPU 占用。

MQL5 / C++
class="kw">return false;
class=class="str">"cmt">//--- Set parameters for the created object
  this.SetObjParams(obj,colour);
class=class="str">"cmt">//--- If there are bound objects
  if(this.ElementsTotal()>class="num">0)
    {
    class=class="str">"cmt">//--- If the panel has auto resize enabled, call the auto resize method
      if(this.AutoSize())
        this.AutoSizeProcess(redraw);
    class=class="str">"cmt">//--- If auto resize is disabled, determine whether scrollbars should be displayed 
      else
       {
       if(this.CheckForOversize())
         {
         class=class="str">"cmt">//--- If the attached objects go beyond the visibility window to the left or right
         if(this.OversizeLeft()>class="num">0 || this.OversizeRight()>class="num">0)
           {
           CScrollBarHorisontal *sbh=this.GetScrollBarHorisontal();
           if(sbh!=NULL)
             {
             sbh.SetThumbParams();
             sbh.SetDisplayed(true);
             sbh.Show();
             }
           }
         class=class="str">"cmt">//--- If the attached objects go beyond the visibility window from above or below
         if(this.OversizeTop()>class="num">0 || this.OversizeBottom()>class="num">0)
           {
           CScrollBarVertical *sbv=this.GetScrollBarVertical();
           if(sbv!=NULL)
             {
             sbv.SetThumbParams();
             sbv.SetDisplayed(true);
             sbv.Show();
             }
           }
         }
       }
    }
class=class="str">"cmt">//--- Crop the created object along the edges of the visible part of the container
  obj.Crop();
class=class="str">"cmt">//--- class="kw">return &class="macro">#x27;true&class="macro">#x27;
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Redraw the object                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CButton::Redraw(class="type">bool redraw)
  {
class=class="str">"cmt">//--- Fill the object with the background class="type">class="kw">color

「复选框与标签的重绘差异在透明度」

在自定义控件里,CCheckBox 和 CLabel 都靠 Redraw 方法刷新画面,但背景擦除的透明度参数并不一致。CCheckBox::Redraw 调用 EraseNoCrop 时传的是 this.Opacity(),而 CLabel::Redraw 直接写死 0,也就是完全透明背景。 CLabel 内部先用 int x=0,y=0 声明坐标变量,再通过 SetTextParamsByAlign(x,y) 按对齐方式算好落点,最后用 this.Text(...) 在对象坐标内绘制文字并 Update。CCheckBox 则多一步 SetCorrectTextCoords(),把文字坐标修正到勾选框旁边,并靠 ShowControlFlag(this.CheckState()) 控制勾选标记的显隐。 如果你在 MT5 里继承了这两类控件做面板,要注意 CLabel 背景恒透明可能盖不住下层元素;想统一观感,可把 CLabel::Redraw 里的 0 改成 this.Opacity() 再编译验证。外汇与贵金属界面开发同样属高风险环境,参数误改可能导致控件闪烁或资源泄漏。

MQL5 / C++
  this.EraseNoCrop(this.BackgroundColor(),this.Opacity(),false);
class=class="str">"cmt">//--- Declare the variables for X and Y coordinates and set their values depending on the text alignment
  class="type">int x=class="num">0,y=class="num">0;
  CLabel::SetTextParamsByAlign(x,y);
class=class="str">"cmt">//--- Draw the text within the set coordinates of the object and the binding point of the text, and update the object 
  this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
  this.Crop();
  this.Update(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Redraw the object                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CCheckBox::Redraw(class="type">bool redraw)
  {
class=class="str">"cmt">//--- Fill the object with the background class="type">class="kw">color having full transparency
  this.EraseNoCrop(this.BackgroundColor(),this.Opacity(),false);
class=class="str">"cmt">//--- Set corrected text coordinates relative to the checkbox
  this.SetCorrectTextCoords();
class=class="str">"cmt">//--- Draw the text and checkbox within the set coordinates of the object and the binding point, and update the object 
  this.Text(this.m_text_x,this.m_text_y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
  this.ShowControlFlag(this.CheckState());
  this.Crop();
  this.Update(redraw);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Redraw the object                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CLabel::Redraw(class="type">bool redraw)
  {
class=class="str">"cmt">//--- Fill the object with the background class="type">class="kw">color having full transparency
  this.EraseNoCrop(this.BackgroundColor(),class="num">0,false);
class=class="str">"cmt">//--- Declare the variables for X and Y coordinates and set their values depending on the text alignment
  class="type">int x=class="num">0,y=class="num">0;
  this.SetTextParamsByAlign(x,y);
class=class="str">"cmt">//--- Draw the text within the set coordinates of the object and the binding point of the text, and update the object 
  this.Text(x,y,this.Text(),this.ForeColor(),this.ForeColorOpacity(),this.TextAnchor());
  this.Crop();
  this.Update(redraw);
  }

◍ 让垂直滚动条跑起来的实测改法

把前一篇的 EA 放到 \MT5\MQL5\Experts\TestDoEasy\Part133\ 目录下,命名成 TestDoEasy133.mq5,就能直接做这一轮验证。 关键改动只有一处:面板里挂的按钮对象,垂直尺寸要做得比父面板大,也就是上下都超出边缘,而宽度仍贴合父面板工作区。其余逻辑不用动。 编译后挂到图表前,先把面板自动大小参数设成 "No"。启动后能看到,垂直滚动条的行为和上一篇做的水平滚动条基本一致——拖拽按钮越界部分即可带动面板内容滚动。 下面这段是创建面板和越界按钮的核心片段,注意高亮那行把按钮高度设成了工作区高度加 50、Y 坐标取 -40,正是它制造了上下溢出:

MQL5 / C++
class=class="str">"cmt">//--- Create the required number of WinForms Panel objects
  CPanel *pnl=NULL;
  for(class="type">int i=class="num">0;i<FORMS_TOTAL;i++)
    {
      pnl=engine.CreateWFPanel("WinForms Panel"+(class="type">class="kw">string)i,(i==class="num">0 ? class="num">50 : class="num">70),(i==class="num">0 ? class="num">50 : class="num">70),class="num">410,class="num">200,array_clr,class="num">200,true,true,false,-class="num">1,FRAME_STYLE_BEVEL,true,false);
      if(pnl!=NULL)
        {
         pnl.Hide();
         class=class="str">"cmt">//--- Set Padding to class="num">4
         pnl.SetPaddingAll(class="num">3);
         class=class="str">"cmt">//--- Set the flags of relocation, auto resizing and auto changing mode from the inputs
         pnl.SetMovable(InpMovable);
         pnl.SetAutoSize(InpAutoSize,false);
         pnl.SetAutoSizeMode((ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)InpAutoSizeMode,false);
         class=class="str">"cmt">//---
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_BUTTON,class="num">10,-class="num">40,pnl.WidthWorkspace()-class="num">30,pnl.HeightWorkspace()+class="num">50,clrNONE,class="num">255,true,false);
         CButton *btn=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_BUTTON,class="num">0);
         btn.SetText("class="num">123456789012345678901234567890");
         pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">40,class="num">20,class="num">60,class="num">20,clrDodgerBlue,class="num">255,false,false);
         CLabel *lbl=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">0);
         lbl.SetText("LABEL");

下一篇要接着做的控件

这一篇收尾时,作者把话挑明了:DoEasy 图形库的下一篇会在一个容器对象上接两条滚动条,并继续堆其他控件。附件里的 MQL5.zip 有 5257.78 KB,你拉下来就能在 MT5 里直接编译跑通现有控件,自己改参数试手感。 评论区有个细节值得记一下:有用户问能不能做带上下按钮的编辑框(类似 MT5 默认的 Lotsize 面板),作者回说「当然可以,但这类控件还没做到那一步,很多东西会逐步补,不会很快」——也就是说眼下库里想直接拖出数值微调框还不现实,别白费劲找。 外汇和贵金属市场高波动、高杠杆,用这类自写库做面板也只是辅助看盘与下单,任何形态识别都不保证胜率,真要接实盘请先在策略测试器里跑够样本。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到控件边界与重绘状态的提示,把重复劳动交给小布,你专注决策。

常见问题

根因是图形元素在被裁剪到父窗体大小之前就被无控制地提前更新并完整渲染,随后才修剪,造成可见残影。修复方式是定位并重绘消除过早更新。
可用事件时间结合图表周期做计算推导开启时间,比先转柱索引再取时间的标准函数更省 CPU,适合对执行速度敏感的库函数。
可以,小布盯盘的品种页已内置控件边界与重绘状态诊断,不需要你自己去图表上逐帧抓闪烁。
不必,标准色往往不够用,可在库里扩展颜色映射,按交互情形把灰推向微红或微蓝等变体,提升控件可读性。
滚动条基础已齐,后续主要补窗体样式的后续控件,具体规划见文末下一阶段,完整脉络参考前两篇基础与水平滚动条篇。