DoEasy. 控件(第三十一部分):滚动条控件内内容的滚动·进阶篇
📜

DoEasy. 控件(第三十一部分):滚动条控件内内容的滚动·进阶篇

(2/3)·滑块缩到极小就失控?两个像素步长与最小尺寸宏如何锁住滚动逻辑

含代码示例偏理论 第 2/3 篇
很多人以为滚动条滑块只是装饰,移动它不会联动容器里的内容。实际滑块大小映射的是超出容器的内容比例,不处理最小尺寸,窄容器下滑块会小到无法点中,滚动直接断链。

◍ 面板工作区尺寸与坐标的取法

在自绘面板类里,工作区指的是扣掉边框和内边距之后真正能画东西的矩形范围。下面这组方法把总宽高、起点坐标、右/下边界都算了出来,核心是用 fmax 取边框与内边距中较大的那一个,避免两者叠加导致绘图区被多扣。 WidthWorkspace 返回可用宽度:总宽减去左边距(Border 与 Padding 取大)再减右边距(同样取大)。HeightWorkspace 同理处理上下方向。CoordXWorkspace 和 CoordYWorkspace 给出工作区左上角在画布里的绝对坐标,RightEdgeWorkspace 与 BottomEdgeWorkspace 则给出右、下两条边的位置。 SetForeColor 负责改全部面板对象的默认文字色。若新颜色和当前一致直接 return;否则写 CANV_ELEMENT_PROP_FORE_COLOR 属性,并在 set_init_color 为真时同步改初始色,方便重置样式。外汇与贵金属图表上自绘面板属于高风险辅助工具,参数误设只影响显示不影响报价。 [CODE] //--- Return the size and coordinates of the working area int WidthWorkspace(void) const { return this.Width()-::fmax(this.BorderSizeLeft(),this.PaddingLeft())-::fmax(this.BorderSizeRight(),this.PaddingRight()); } int HeightWorkspace(void) const { return this.Height()-::fmax(this.BorderSizeTop(),this.PaddingTop())-::fmax(this.BorderSizeBottom(),this.PaddingBottom());} int CoordXWorkspace(void) const { return this.CoordX()+::fmax(this.BorderSizeLeft(),this.PaddingLeft()); } int CoordYWorkspace(void) const { return this.CoordY()+::fmax(this.BorderSizeTop(),this.PaddingTop()); } int RightEdgeWorkspace(void) const { return this.RightEdge()-::fmax(this.BorderSizeRight(),this.PaddingRight()); } int BottomEdgeWorkspace(void) const { return this.BottomEdge()-::fmax(this.BorderSizeBottom(),this.PaddingBottom()); } //--- (1) Set and (2) return the default text color of all panel objects void SetForeColor(const color clr,const bool set_init_color) { if(this.ForeColor()==clr) return; this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,clr); if(set_init_color) this.SetForeColorInit(clr); [/CODE]

MQL5 / C++
class=class="str">"cmt">//--- Return the size and coordinates of the working area
  class="type">int                 WidthWorkspace(class="type">void)      const { class="kw">return this.Width()-::fmax(this.BorderSizeLeft(),this.PaddingLeft())-::fmax(this.BorderSizeRight(),this.PaddingRight()); }
  class="type">int                 HeightWorkspace(class="type">void)     const { class="kw">return this.Height()-::fmax(this.BorderSizeTop(),this.PaddingTop())-::fmax(this.BorderSizeBottom(),this.PaddingBottom());}
  class="type">int                 CoordXWorkspace(class="type">void)     const { class="kw">return this.CoordX()+::fmax(this.BorderSizeLeft(),this.PaddingLeft());                                           }
  class="type">int                 CoordYWorkspace(class="type">void)     const { class="kw">return this.CoordY()+::fmax(this.BorderSizeTop(),this.PaddingTop());                                                 }
  class="type">int                 RightEdgeWorkspace(class="type">void)  const { class="kw">return this.RightEdge()-::fmax(this.BorderSizeRight(),this.PaddingRight());                                            }
  class="type">int                 BottomEdgeWorkspace(class="type">void) const { class="kw">return this.BottomEdge()-::fmax(this.BorderSizeBottom(),this.PaddingBottom());                                       }
class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the class="kw">default text class="type">class="kw">color of all panel objects
  class="type">void                SetForeColor(const class="type">class="kw">color clr,const class="type">bool set_init_color)
                    {
                      if(this.ForeColor()==clr)
                        class="kw">return;
                      this.SetProperty(CANV_ELEMENT_PROP_FORE_COLOR,clr);
                      if(set_init_color)
                        this.SetForeColorInit(clr);

「容器控件的边界溢出与依赖对象管理」

在 MT5 自定义 GUI 框架里,CWinFormBase 用私有结构 SOversizes 记录子对象超出容器四边的像素量:top、bottom、left、right 均为 int 类型,由 m_oversize 实例持有。CheckForOversize() 返回布尔值,用来判断绑定的控件是否越过了容器边框,实盘面板若动态增删按钮,这一步能避免图形错位。 Resize() 有两个重载:一个改当前对象尺寸(w,h,redraw),另一个按 index 改指定子对象。ShiftDependentObj(shift_x,shift_y) 则统一平移所有依赖对象,适合做整体拖拽。GetMaxLongPropFromDependent / GetMinLongPropFromDependent 遍历附属对象,提取某 ENUM_CANV_ELEMENT_PROP_INTEGER 属性的最大或最小长整值,比如批量对齐宽度时直接拿极值。 m_list_active_elements 是指向活跃元素列表的指针,m_fore_color_init 与 m_fore_state_on_color_init 分别存控件文本初始色和「ON」状态色。外汇与贵金属图表挂这类 UI 属高风险操作,参数误写可能让 EA 面板卡死,建议在策略测试器先跑空图验证。

MQL5 / C++
class="kw">protected:
  CArrayObj            *m_list_active_elements;            class=class="str">"cmt">// Pointer to the list of active elements
  class="type">class="kw">color                 m_fore_color_init;                 class=class="str">"cmt">// Initial class="type">class="kw">color of the control text
  class="type">class="kw">color                 m_fore_state_on_color_init;        class=class="str">"cmt">// Initial class="type">class="kw">color of the control text when the control is "ON"
class="kw">private:
  class="kw">struct SOversizes                                     class=class="str">"cmt">// Structure of values for bound objects leaving the container
   {
     class="type">int  top;     class=class="str">"cmt">// top
     class="type">int  bottom;  class=class="str">"cmt">// bottom
     class="type">int  left;    class=class="str">"cmt">// left
     class="type">int  right;   class=class="str">"cmt">// right
   };
  SOversizes            m_oversize;                       class=class="str">"cmt">// Structure of values for leaving the container
class=class="str">"cmt">//--- Return the font flags
  class="type">uint                  GetFontFlags(class="type">void);
class="kw">public:
class=class="str">"cmt">//--- Redraw the object
  class="kw">virtual class="type">void          Redraw(class="type">bool redraw);
class=class="str">"cmt">//--- Set the new size for the(class="num">1) current object and(class="num">2) the object specified by index
  class="kw">virtual class="type">bool          Resize(const class="type">int w,const class="type">int h,const class="type">bool redraw);
  class="kw">virtual class="type">bool          Resize(const class="type">int index,const class="type">int w,const class="type">int h,const class="type">bool redraw);
class=class="str">"cmt">//--- Return the flag of the container content leaving the container borders
  class="type">bool                  CheckForOversize(class="type">void);
class=class="str">"cmt">//--- Shift all bound objects
  class="type">bool                  ShiftDependentObj(const class="type">int shift_x,const class="type">int shift_y);
class=class="str">"cmt">//--- Return the(class="num">1) maximum and(class="num">2) minimum values of the specified integer class="kw">property from all attached objects to the current one
  class="type">long                  GetMaxLongPropFromDependent(const ENUM_CANV_ELEMENT_PROP_INTEGER prop);
  class="type">long                  GetMinLongPropFromDependent(const ENUM_CANV_ELEMENT_PROP_INTEGER prop);
class="kw">protected:
class=class="str">"cmt">//--- Protected constructor with object type, chart ID and subwindow
                       CWinFormBase(const ENUM_GRAPH_ELEMENT_TYPE type,

容器越界像素的四个读取口

在 MT5 的 GUI 容器类里,附属对象很可能画出容器边界。框架用 m_oversize 结构体记录上、下、左、右四个方向超出的像素数,供后续重绘或裁剪判断。 下面四个内联方法直接返回对应方向的越界像素值,全部为 const 成员函数,调用零开销: OversizeTop 返回 m_oversize.top,即附属对象超出容器顶部的像素;OversizeBottom 返回 m_oversize.bottom,即底部超出量;OversizeLeft 对应左侧的 m_oversize.left;OversizeRight 对应右侧的 m_oversize.right。 实际排错时,若 OversizeRight 返回 12,说明有对象向右越界 12 像素,可能遮挡坐标轴。打开 MT5 导航栏的自定义指标,挂上容器后打印这四个值即可定位越界方向。外汇与贵金属图表插件开发属高风险调试,参数错误可能致图表卡死。

MQL5 / C++
class="type">int OversizeTop(class="type">void) const { class="kw">return this.m_oversize.top; }
class="type">int OversizeBottom(class="type">void) const { class="kw">return this.m_oversize.bottom; }
class="type">int OversizeLeft(class="type">void) const { class="kw">return this.m_oversize.left; }
class="type">int OversizeRight(class="type">void) const { class="kw">return this.m_oversize.right; }

◍ 窗体基类的构造函数怎么把默认状态铺好

在 MT5 自定义图形界面库里,CWinFormBase 承担所有窗体控件的底层容器角色。它的带坐标构造函数先透传 type、主画布对象、基底对象、图表 ID、子窗口和 x/y/w/h 给 CForm,再由自身把元素类型钉死为 GRAPH_ELEMENT_TYPE_WF_BASE,同时把库内对象类型标记为 OBJECT_DE_TYPE_GWF_BASE。 紧接着是一轮变量初始化:文本清空、前景色取 CLR_DEF_FORE_COLOR 并同步三种鼠标状态色(常态/按下/悬停均先指向同一前景色),前景不透明度走 CLR_DEF_FORE_COLOR_OPACITY,字体粗细 FW_TYPE_NORMAL,四向 margin、padding、border 全设 0,停靠模式 CANV_ELEMENT_DOCK_MODE_NONE 且不重绘,边框风格 FRAME_STYLE_NONE,自动尺寸关闭。 坐标由 CForm::SetCoordXInit 等四个方法落初值;阴影 m_shadow=false,渐变走竖直 m_gradient_v=true、关闭对角 m_gradient_c=false,活跃元素表 new 一个 CArrayObj,最后用 ZeroMemory 把 m_oversize 清零。另一重载构造函数只接收 main_obj、base_obj、chart_id、subwindow、descript 与坐标,把 type 写死为 GRAPH_ELEMENT_TYPE_WF_BASE 后做同样的元素类型设置。 开 MT5 新建一个继承 CWinFormBase 的面板类,断点打在构造函数末尾,能看到 border 与 padding 默认全 0、m_gradient_v 为 true——若你之后发现控件间距异常,先回来核对这里是不是被后续代码覆盖。

MQL5 / C++
const class="type">int x,
              const class="type">int y,
              const class="type">int w,
              const class="type">int h) : CForm(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.m_type=OBJECT_DE_TYPE_GWF_BASE;
class=class="str">"cmt">//--- Initialize all variables
   this.SetText("");
   this.SetForeColor(CLR_DEF_FORE_COLOR,true);
   this.SetForeStateOnColor(this.ForeColor(),true);
   this.SetForeStateOnColorMouseDown(this.ForeColor());
   this.SetForeStateOnColorMouseOver(this.ForeColor());
   this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
   this.SetFontBoldType(FW_TYPE_NORMAL);
   this.SetMarginAll(class="num">0);
   this.SetPaddingAll(class="num">0);
   this.SetBorderSizeAll(class="num">0);
   this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
   this.SetBorderStyle(FRAME_STYLE_NONE);
   this.SetAutoSize(false,false);
   CForm::SetCoordXInit(x);
   CForm::SetCoordYInit(y);
   CForm::SetWidthInit(w);
   CForm::SetHeightInit(h);
   this.m_shadow=false;
   this.m_gradient_v=true;
   this.m_gradient_c=false;
   this.m_list_active_elements=new CArrayObj();
   ::ZeroMemory(this.m_oversize);
  }
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">//+------------------------------------------------------------------+
CWinFormBase::CWinFormBase(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) : CForm(GRAPH_ELEMENT_TYPE_WF_BASE,main_obj,base_obj,chart_id,subwindow,descript,x,y,w,h)
  {
class=class="str">"cmt">//--- Set the graphical element and library object types as a base WinForms object
   this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_BASE);

「GUI 容器初始化与越界检测的内部逻辑」

在 MT5 自定义 GUI 框架里,CWinFormBase 的构造函数先把对象类型锁成 OBJECT_DE_TYPE_GWF_BASE,随后一口气把文字、前景色、边框、间距等变量全部归零或设成默认。像 SetMarginAll(0)、SetBorderSizeAll(0)、SetDockMode(NONE,false) 这类调用,意味着新建的窗体默认不带外边距和边框,也不参与自动停靠,坐标和尺寸完全由外部传入的 x、y、w、h 决定。 初始化末尾还干了两件容易被忽略的事:m_shadow 置 false 关掉阴影,m_gradient_v 置 true 开垂直渐变;同时 new 一个 CArrayObj 作为活动元素列表,并用 ZeroMemory 把 m_oversize 结构清掉。 越界检测函数 CheckForOversize 的思路很直接:每次调用先 ZeroMemory 重置 m_oversize,再遍历 ElementsTotal() 个挂接对象。循环里主动跳过三种滚动条类型(横向、纵向及基类滚动条),只对普通子元素算右溢出 r=obj.RightEdge()-RightEdgeWorkspace() 和左溢出 l=CoordXWorkspace()-obj.CoordX(),正数且大于已记录值才写回结构。 这套机制对外汇或贵金属 EA 面板有意义:子控件超出工作区可能触发滚动条显示逻辑,但 GUI 渲染异常本身不影响报价,交易端仍属高风险环境,参数误设可能导致面板不可见。开 MT5 把这段塞进自己的 CWinFormBase 派生类,改 ZeroMemory 前后打印 m_oversize 各字段,就能验证溢出值何时被填充。

MQL5 / C++
this.m_type=OBJECT_DE_TYPE_GWF_BASE;
class=class="str">"cmt">//--- Initialize all variables
this.SetText("");
this.SetForeColor(CLR_DEF_FORE_COLOR,true);
this.SetForeStateOnColor(this.ForeColor(),true);
this.SetForeStateOnColorMouseDown(this.ForeColor());
this.SetForeStateOnColorMouseOver(this.ForeColor());
this.SetForeColorOpacity(CLR_DEF_FORE_COLOR_OPACITY);
this.SetFontBoldType(FW_TYPE_NORMAL);
this.SetMarginAll(class="num">0);
this.SetPaddingAll(class="num">0);
this.SetBorderSizeAll(class="num">0);
this.SetDockMode(CANV_ELEMENT_DOCK_MODE_NONE,false);
this.SetBorderStyle(FRAME_STYLE_NONE);
this.SetAutoSize(false,false);
CForm::SetCoordXInit(x);
CForm::SetCoordYInit(y);
CForm::SetWidthInit(w);
CForm::SetHeightInit(h);
this.m_shadow=false;
this.m_gradient_v=true;
this.m_gradient_c=false;
this.m_list_active_elements=new CArrayObj();
::ZeroMemory(this.m_oversize);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of the container content leaving its borders     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWinFormBase::CheckForOversize(class="type">void)
  {
class=class="str">"cmt">//--- Update the structure of values for bound objects leaving the container
  ::ZeroMemory(this.m_oversize);
class=class="str">"cmt">//--- In the loop by the number of attached objects
  for(class="type">int i=class="num">0;i<this.ElementsTotal();i++)
    {
    class=class="str">"cmt">//--- Get the next object and skip scrollbars
    CWinFormBase *obj=this.GetElement(i);
    if(obj==NULL                                                                              ||
       obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR                              ||
       obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL                  ||
       obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL)
       class="kw">continue;
    class=class="str">"cmt">//--- Get the value in pixels of the object leaving the form at the right
    class=class="str">"cmt">//--- If the value is greater than zero and greater than the one written in the structure field, save it in the structure
    class="type">int r=obj.RightEdge()-this.RightEdgeWorkspace();
    if(r>class="num">0 && r>this.m_oversize.right)
       this.m_oversize.right=r;
    class=class="str">"cmt">//--- Get the value in pixels of the object leaving the form at the left
    class=class="str">"cmt">//--- If the value is greater than zero and greater than the one written in the structure field, save it in the structure
    class="type">int l=this.CoordXWorkspace()-obj.CoordX();
    if(l>class="num">0 && l>this.m_oversize.left)
       this.m_oversize.left=l;

越界像素捕获与从属对象批量平移

窗体类在重绘前会先扫描所有绑定对象,把超出边框的像素量记进 m_oversize 结构。上边距用工作区顶坐标减对象顶坐标,下边距用对象底边缘减工作区底边缘,只要大于 0 且刷新了历史最大值就写入,四个方向任一越界都返回 true 触发后续处理。 实际平移由 ShiftDependentObj(shift_x, shift_y) 完成:遍历 ElementsTotal() 个对象,跳过三种滚动条类型,其余对象坐标直接加偏移量后调用 Move(x, y, false)。若任意一个 Move 失败立即返回 false,保证窗体位移的原子性。 成功移动后必须重算相对坐标——用绝对坐标减窗体自身坐标写回 SetCoordXRelative / SetCoordYRelative,再 Redraw(false) 避免闪烁。在 MT5 里把 shift_x 设成 10、shift_y 设成 -5 跑一遍,能直观看到子对象跟随窗体同步错位。 GetMaxLongPropFromDependent 则反过来从下属对象捞整数属性极值,property 初值设为 -LONG_MAX 确保任何合法属性值都能覆盖,这种初始化手法在写自定义控件遍历时值得直接抄。

MQL5 / C++
  class=class="str">"cmt">//--- Get the value in pixels of the object leaving the form at the top
  class=class="str">"cmt">//--- If the value is greater than zero and greater than the one written in the structure field, save it in the structure
  class="type">int t=this.CoordYWorkspace()-obj.CoordY();
  if(t>class="num">0 && t>this.m_oversize.top)
     this.m_oversize.top=t;
  class=class="str">"cmt">//--- Get the value in pixels of the object leaving the form at the bottom
  class=class="str">"cmt">//--- If the value is greater than zero and greater than the one written in the structure field, save it in the structure
  class="type">int b=obj.BottomEdge()-this.BottomEdgeWorkspace();
  if(b>class="num">0 && b>this.m_oversize.bottom)
     this.m_oversize.bottom=b;
   }
class=class="str">"cmt">//--- Return the flag indicating that at least one side of the attached object goes beyond the form borders
   class="kw">return(m_oversize.top>class="num">0 || m_oversize.bottom>class="num">0 || m_oversize.left>class="num">0 || m_oversize.right>class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+-------------------------------------------+
class=class="str">"cmt">//| Shift all bound objects                    |
class=class="str">"cmt">//+-------------------------------------------+
class="type">bool CWinFormBase::ShiftDependentObj(const class="type">int shift_x,const class="type">int shift_y)
  {
class=class="str">"cmt">//--- In the loop by all bound objects,
  for(class="type">int i=class="num">0;i<this.ElementsTotal();i++)
    {
      class=class="str">"cmt">//--- get the next object and skip scrollbars
      CWinFormBase *obj=this.GetElement(i);
      if(obj==NULL                                                        ||
         obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR         ||
         obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL ||
         obj.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL
         ) class="kw">continue;
      class=class="str">"cmt">//--- Set the offset coordinates
      class="type">int x=obj.CoordX()+shift_x;
      class="type">int y=obj.CoordY()+shift_y;
      if(!obj.Move(x,y,false))
         class="kw">return false;
      class=class="str">"cmt">//--- After a successful offset, set relative coordinates and redraw the object
      obj.SetCoordXRelative(obj.CoordX()-this.CoordX());
      obj.SetCoordYRelative(obj.CoordY()-this.CoordY());
      obj.Redraw(false);
    }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the maximum value of the specified integer                |
class=class="str">"cmt">//| class="kw">property from all objects subordinate to the base one            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long CWinFormBase::GetMaxLongPropFromDependent(const ENUM_CANV_ELEMENT_PROP_INTEGER prop)
  {
class=class="str">"cmt">//--- Initialize &class="macro">#x27;class="kw">property&class="macro">#x27; with -class="num">1
   class="type">long class="kw">property=-LONG_MAX;
class=class="str">"cmt">//--- In the loop through the list of bound objects
把滑块联动诊断交给小布
这些控件边界与步长参数小布盯盘的 AIGC 已内置,打开对应品种页即可看到容器偏移的可视化提示,你只需判断要不要改步长。

常见问题

滑块是容器宽度的示意,超出容器外的内容越多滑块越小,它指示当前窗口内可见内容占整体的比例。
容器宽度极大减小时滑块可能变得太小无法操作,设置宏定义的最小尺寸可避免滚动控制失效。
原文取两像素步长,比 MetaEditor 的六像素更细,具体可按控件响应精度需要调整。
可以,小布盯盘内置了图形元素边缘坐标与步长诊断,打开品种页就能看到容器平移时的参数状态。
原方法只返回计算值而未写入对象属性,需在变更边框的方法里显式赋值才能同步。