DoEasy. 控件(第三十一部分):滚动条控件内内容的滚动·进阶篇
(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]
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 面板卡死,建议在策略测试器先跑空图验证。
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 导航栏的自定义指标,挂上容器后打印这四个值即可定位越界方向。外汇与贵金属图表插件开发属高风险调试,参数错误可能致图表卡死。
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——若你之后发现控件间距异常,先回来核对这里是不是被后续代码覆盖。
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 各字段,就能验证溢出值何时被填充。
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 确保任何合法属性值都能覆盖,这种初始化手法在写自定义控件遍历时值得直接抄。
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