DoEasy. 控件(第三十一部分):滚动条控件内内容的滚动·综合运用
(3/3)·滚动条点了没反应?这篇把按钮、滑块、内容平移三者的联动一次补齐
遍历子对象抠出极值属性
在 MT5 自定义控件库里,基类 CWinFormBase 常需要统计所有从属对象的某个整型属性极值。下面两段方法分别处理最大值与最小值,逻辑对称,但初始化种子不同:最大值用 0 起底,最小值直接用 LONG_MAX 兜底。 循环里先按索引取元素,遇到空指针或三种滚动条类型(横向、纵向、通用)直接 continue,避免把滚动条的尺寸/位置掺进业务布局计算。这个跳过动作很关键,否则右键拖出滚动条时会污染你算出来的边界值。 对每个有效对象,先比自身 GetProperty(prop) 与当前 property,更大(或更小)就覆盖;再递归拉取它下游依赖对象的同属性极值,做二次比较。递归深度取决于你窗体树的层级,实测 5 层以内开销可忽略。 代码逐行拆一下最大值那段:for 从 0 扫到 ElementsTotal()-1;obj 取第 i 个元素;NULL 和三类滚动条continue;obj.GetProperty(prop)>property 则更新;再对依赖树取最大;最后 return property。最小值只是把 > 换成 < 且种子换成 LONG_MAX。
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">//--- If the class="kw">property value of the received object is greater than the value set in the class="kw">property, class=class="str">"cmt">//--- set the current object class="kw">property value to &class="macro">#x27;class="kw">property&class="macro">#x27; if(obj.GetProperty(prop)>class="kw">property) class="kw">property=obj.GetProperty(prop); class=class="str">"cmt">//--- Get the maximum class="kw">property value from objects bound to the current one class="type">long prop_form=obj.GetMaxLongPropFromDependent(prop); class=class="str">"cmt">//--- If the received value is greater than the &class="macro">#x27;class="kw">property&class="macro">#x27; value class=class="str">"cmt">//--- set the received value to &class="macro">#x27;class="kw">property&class="macro">#x27; if(prop_form>class="kw">property) class="kw">property=prop_form; } class=class="str">"cmt">//--- Return the found maximum class="kw">property value class="kw">return class="kw">property; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the minimum 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::GetMinLongPropFromDependent(const ENUM_CANV_ELEMENT_PROP_INTEGER prop) { class=class="str">"cmt">//--- Initialize &class="macro">#x27;class="kw">property&class="macro">#x27; using the LONG_MAX value class="type">long class="kw">property=LONG_MAX; class=class="str">"cmt">//--- In the loop through the list of 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">//--- If the value of the obtained object class="kw">property is less than the value set in &class="macro">#x27;class="kw">property&class="macro">#x27;, class=class="str">"cmt">//--- set the current object class="kw">property value to &class="macro">#x27;class="kw">property&class="macro">#x27; if(obj.GetProperty(prop)<class="kw">property) class="kw">property=obj.GetProperty(prop); class=class="str">"cmt">//--- Get the minimum class="kw">property value from bound objects to the current one class="type">long prop_form=obj.GetMinLongPropFromDependent(prop); class=class="str">"cmt">//--- If the obtained value is less than the class="kw">property value class=class="str">"cmt">//--- set the received value to &class="macro">#x27;class="kw">property&class="macro">#x27;
◍ 滚动条滑块类的鼠标事件拆解
在 MT5 自定义控件库里,CScrollBarThumb 继承自 CButton,专门承载滚动条上可拖拽的滑块逻辑。它把光标在活动区内的三种状态拆成独立虚函数,方便在派生类里重写交互细节。 被高亮的三个 protected 方法分别处理:光标悬停未按键(MouseActiveAreaNotPressedHandler)、任意键按下的拖拽起始(MouseActiveAreaPressedHandler)、以及左键释放的落点(MouseActiveAreaReleasedHandler)。这种拆分让滑块在 1~2 像素级微移时也不会误触发点击事件。 绿色标记的 OnMouseEventPostProcessing 是 public 虚函数,在每轮鼠标事件尾段调用,适合做滑块位置越界修正或重绘收尾。 直接把下面这段类声明贴进 MT5 头文件,就能以 CScrollBarThumb 为基类写自己的滚动条组件,编译后从 OnInit 里 new 一个实例挂到图表子窗口验证交互。
class CScrollBarThumb : class="kw">public CButton { class="kw">private: class="kw">protected: class=class="str">"cmt">//--- &class="macro">#x27;The cursor is inside the active area, the mouse buttons are not clicked&class="macro">#x27; event handler class="kw">virtual class="type">void MouseActiveAreaNotPressedHandler(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="macro">#x27;The cursor is inside the active area, a mouse button is clicked(any)&class="macro">#x27; event handler class="kw">virtual class="type">void MouseActiveAreaPressedHandler(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="macro">#x27;The cursor is inside the active area, the left mouse button is clicked&class="macro">#x27; event handler class="kw">virtual class="type">void MouseActiveAreaReleasedHandler(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">//--- Protected constructor with object type, chart ID and subwindow CScrollBarThumb(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); class="kw">public: class=class="str">"cmt">//--- Last mouse event handler class="kw">virtual class="type">void OnMouseEventPostProcessing(class="type">void); class=class="str">"cmt">//--- Constructor
「滚动条滑块的层叠与鼠标事件接管」
在自定义 MT5 图形界面里,CScrollBarThumb 作为滚动条滑块,其构造函数需同时持有主对象指针 main_obj 与基底对象指针 base_obj,并传入图表 ID、子窗口号、描述文本及 x/y/w/h 几何参数,才能在画布上正确锚定。 滑块的两个核心事件处理函数,都先通过 GetBase() 取回所属窗体基类指针,判空后调用 BringToTop() 把承载它的窗体提到 Z 轴最前。这意味着只要光标落在滑块激活区,无论鼠标键是否按下,关联窗体都会被前置,避免被其他面板遮挡。 MouseActiveAreaNotPressedHandler 与 MouseActiveAreaPressedHandler 在做了前置处理后,分别把事件透传给 CButton 的对应虚函数,保证按钮原有的悬停、按压视觉反馈不丢失。实盘写自定义悬浮面板时,这套机制能让多图表上的控件互不打架。 外汇与贵金属图表挂自定义 UI 属高风险操作,脚本异常可能导致终端卡顿,建议先在模拟盘验证。
CScrollBarThumb(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); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| &class="macro">#x27;The cursor is inside the active area, | class=class="str">"cmt">//| no mouse buttons are clicked&class="macro">#x27; event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CScrollBarThumb::MouseActiveAreaNotPressedHandler(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) { base.BringToTop(); } CButton::MouseActiveAreaNotPressedHandler(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+-------------------------------------------+ class=class="str">"cmt">//| &class="macro">#x27;The cursor is inside the active area, | class=class="str">"cmt">//| a mouse button is clicked(any) | class=class="str">"cmt">//+-------------------------------------------+ class="type">void CScrollBarThumb::MouseActiveAreaPressedHandler(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) { base.BringToTop(); } CButton::MouseActiveAreaPressedHandler(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+
滚动条滑块的鼠标收尾与状态复位
自定义滚动条滑块(CScrollBarThumb)在鼠标释放时要先抢到所属窗体焦点:MouseActiveAreaReleasedHandler 里用 GetBase() 取到基类指针,非空就调 BringToTop(),随后把事件抛给 CButton 的同类 handler 继续走标准按钮逻辑。 真正管「光标离开控件后外观怎么掉回去」的是 OnMouseEventPostProcessing()。它先判 IsVisible() 和 Enabled(),任一不满足直接 return,避免隐藏或禁用状态下还重绘。 当鼠标状态落在 MOUSE_FORM_STATE_OUTSIDE_FORM_* 三个分支(光标在窗体外、无论按没按、滚没滚轮),且上一帧鼠标事件曾是 INSIDE_ACTIVE_AREA_NOT_PRESSED 或 INSIDE_FORM_NOT_PRESSED 时,就把背景、前景、边框颜色一键复位到 State() 决定的初始或开启色,并把 m_mouse_event_last 置为当前 state 对应的 NO_EVENT,最后 Redraw(false) 不强制立刻刷新。 这套写法能保证滑块被拖出活动区外松手后,不会卡在「按下高亮」的残影里。开 MT5 新建一个带 CScrollBar 的面板,故意把滑块拖到窗体外释放,观察颜色是否如代码所述回到 BackgroundColorInit()。
class="type">void CScrollBarThumb::MouseActiveAreaReleasedHandler(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) { base.BringToTop(); } CButton::MouseActiveAreaReleasedHandler(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+-------------------------------------------+ class=class="str">"cmt">//| Last mouse event handler | class=class="str">"cmt">//+-------------------------------------------+ class="type">void CScrollBarThumb::OnMouseEventPostProcessing(class="type">void) { if(!this.IsVisible() || !this.Enabled()) class="kw">return; ENUM_MOUSE_FORM_STATE state=GetMouseState(); class="kw">switch(state) { class=class="str">"cmt">//--- The cursor is outside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is outside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is outside the form, the mouse wheel is being scrolled case MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL : if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED) { this.SetBackgroundColor(this.State() ? this.BackgroundStateOnColor() : this.BackgroundColorInit(),false); this.SetForeColor(this.State() ? this.ForeStateOnColor() : this.ForeColorInit(),false); this.SetBorderColor(this.BorderColorInit(),false); this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT); this.Redraw(false); } break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse button(any) is clicked class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse wheel is being scrolled
◍ 鼠标状态枚举的归并写法
在 MT5 自定义面板里处理鼠标交互时,系统会吐出几十种 MOUSE_FORM_STATE_* 状态。上面这组 case 把窗口内、激活区、滚动条区、四边缩放区的『未按下 / 已按下 / 滚轮 / 释放』全部并列,等于告诉事件处理器:这些区域当前都不需要单独逻辑,统一走默认分支即可。 具体看,光窗口缩放就拆了上、下、左、右四个方向,每个方向又分 NOT_PRESSED、PRESSED、WHEEL 三种,加上顶部注释里提到的『光标在缩放区但没点鼠标』和『任意键按下』,仅缩放相关状态就有 12 个以上枚举值。 实际写 EA 或指标界面时,若你只关心『点击激活区按钮』这一类事件,就可以像这样把其余 20 多个状态用空 case 吞掉,避免 OnChartEvent 里堆满无意义的 if。开 MT5 新建一个 panel 脚本,把这段 case 贴进 CHARTEVENT_MOUSE_MOVE 分支,编译后移动鼠标到边框,日志不报错即说明归并生效。
class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse buttons are not clicked class=class="str">"cmt">//--- The cursor is within the window resizing area, the mouse button(any) is clicked class=class="str">"cmt">//--- The cursor is within the window separator area, the mouse wheel is being scrolled case MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_INSIDE_FORM_WHEEL : class=class="str">"cmt">//--- Within the active area case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_RELEASED : class=class="str">"cmt">//--- Within the scrolling area at the bottom case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_BOTTOM_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_BOTTOM_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_BOTTOM_WHEEL : class=class="str">"cmt">//--- Within the scrolling area to the right case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_RIGHT_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_RIGHT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_RIGHT_WHEEL : class=class="str">"cmt">//--- Within the window resizing area at the top case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_AREA_WHEEL : class=class="str">"cmt">//--- Within the window resizing area at the bottom case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_AREA_WHEEL : class=class="str">"cmt">//--- Within the window resizing area to the left case MOUSE_FORM_STATE_INSIDE_RESIZE_LEFT_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_LEFT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_LEFT_AREA_WHEEL : class=class="str">"cmt">//--- Within the window resizing area to the right case MOUSE_FORM_STATE_INSIDE_RESIZE_RIGHT_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_RIGHT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_RIGHT_AREA_WHEEL :
「滚动条捕获区与鼠标分区断点」
在自定义表单控件里,鼠标状态被拆得很细:光是四个角的拖拽缩放区,就各自分了未按下、按下、滚轮三种子状态,加上控制区同样三组,合计 15 个 case 落在同一段 switch 里直接 break,不干额外活。 这种写法意味着:当光标处于窗体边缘或内部控件区时,框架默认不拦截事件,把处理权留给你后续在别处补逻辑。开 MT5 用调试面板抓一下 MOUSE_FORM_STATE_* 枚举,能确认这些常量在 resize 时确实只走空分支。 CScrollBar::CreateThumbArea 实际只调了 CreateArrowButtons,两个参数都传了 DEF_CONTROL_SCROLL_BAR_WIDTH,说明水平/垂直滚动条的箭头按钮默认是正方形。而 CalculateThumbAreaSize 写死返回 DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN,也就是滑块最小尺寸就是捕获区尺寸下限,想让滑块可点中你得在外层重算。 CScrollBarHorisontal 继承 CScrollBar 后,把 CreateArrowButtons 标成 virtual 私有重写,水平条箭头的宽高传入逻辑和基类不同,具体差值要看 DEF_CONTROL_SCROLL_BAR_WIDTH 在 include 里的实际像素值(通常 10~16)。外汇贵金属面板做自定义控件时,这类尺寸硬编码在高频缩放下可能露边,建议自己覆写算一遍。
case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_LEFT_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_LEFT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_LEFT_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_RIGHT_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_RIGHT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_TOP_RIGHT_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_LEFT_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_LEFT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_LEFT_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_RIGHT_AREA_NOT_PRESSED: case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_RIGHT_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_BOTTOM_RIGHT_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_CONTROL_AREA_WHEEL : break; class="kw">default: break; class="type">void CScrollBar::CreateThumbArea(class="type">void) { this.CreateArrowButtons(DEF_CONTROL_SCROLL_BAR_WIDTH,DEF_CONTROL_SCROLL_BAR_WIDTH); } class="type">int CScrollBar::CalculateThumbAreaSize(class="type">void) { class="kw">return DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN; } class CScrollBarHorisontal : class="kw">public CScrollBar { class="kw">private: class="kw">virtual class="type">void CreateArrowButtons(const class="type">int width,const class="type">int height);
横向滚动条类的受保护构造与滑块区域接口
在自定义图形库里,横向滚动条 CScrollBarHorisontal 把构造过程收进 protected 区,外部只能拿到实例后调公开方法,不能直接 new。构造函数接收图形元素类型、主/基对象指针、图表 ID、子窗口号、描述串以及 x/y/w/h 八项参数,等于把坐标与归属一次性绑死。 公开区里三个 SupportProperty 重载全部返回 true,意味着整型、双精度、字符串三类画布属性该滚动条都吃,方便上层直接读写状态而不用逐个判断。 获取左右箭头按钮靠 GetElementByType 按类型检索,索引传 0:左键类型 GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT,右键对应 RIGHT。这两个指针让调用方可以直接改箭头显隐或绑定事件。 BarWorkAreaSize 与 BarWorkAreaCoord 返回滑块可用区的像素尺寸和起点坐标,是算拖拽映射的关键。Resize 除了改 w/h 还带 redraw 开关,设 false 可批量改完再统一重绘,少一次闪屏。 CalculateThumbAreaDistance 按 thumb_size 算滑块捕获区距离,配合后面『计算并设置捕获区参数』的注释,说明拖拽手感是代码层可控的,不是系统默认。
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 CScrollBarHorisontal(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); 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(class="num">1) left and(class="num">2) right arrow button CArrowLeftButton *GetArrowButtonLeft(class="type">void) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT,class="num">0); } CArrowRightButton*GetArrowButtonRight(class="type">void) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT,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)
◍ 横向滚动条的尺寸重算与右箭头重定位
在自定义控件库里,CScrollBarHorisontal 的 Resize 方法承担了滚动条整体变形时子控件的联动职责。它先调用基类 CWinFormBase::Resize 改变外框尺寸,若基类返回失败则直接退出,避免后续坐标计算出错。 随后通过 GetArrowRightButton 取出右侧箭头按钮对象,空指针时同样返回 false。这一守卫判断很关键,因为 MT5 图形对象在图表重载过程中可能短暂失效,直接解引用会触发运行时异常。 拿到按钮后,代码把它移动到滚动条右边缘内缩 BorderSizeRight 与按钮宽度的位置,再重设按钮的相对坐标(相对于滚动条原点)。最后调用 SetThumbParams 刷新滑块比例,整段返回 true 表示重算成功。 在 MT5 里验证时,可故意把滚动条宽度从 200 改为 80,观察右箭头是否自动贴边;外汇与贵金属图表控件涉及高频重绘,此类操作存在图形资源竞争的高风险,建议在 OnTimer 之外少做强制 Resize。
class="type">int SetThumbParams(class="type">void); class=class="str">"cmt">//--- Constructor CScrollBarHorisontal(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); 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">//+-------------------------------------------+ class=class="str">"cmt">//| Set the new size | class=class="str">"cmt">//+-------------------------------------------+ class="type">bool CScrollBarHorisontal::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 right arrow CArrowRightButton *br=this.GetArrowButtonRight(); class=class="str">"cmt">//--- If the button is not received, class="kw">return &class="macro">#x27;false&class="macro">#x27; if(br==NULL) class="kw">return false; class=class="str">"cmt">//--- Move the button to the right edge of the scrollbar if(br.Move(this.RightEdge()-this.BorderSizeRight()-br.Width(),br.CoordY())) { class=class="str">"cmt">//--- Set new relative coordinates for the button br.SetCoordXRelative(br.CoordX()-this.CoordX()); br.SetCoordYRelative(br.CoordY()-this.CoordY()); } class=class="str">"cmt">//--- Set the slider parameters this.SetThumbParams(); class=class="str">"cmt">//--- Successful class="kw">return true; }
「横向滚动条滑块尺寸与位移的算法落点」
在 MT5 自定义控件里,横向滚动条的滑块(thumb)不是写死像素的,而是按容器可见区与内容总宽的比例实时算出来的。核心思路是先拿到基类工作区宽度 base_w,再把左右 oversize 加进去得到 objs_w,用 base_w*100.0/objs_w 求出可见比例 px,滑块长度就按滚动条工作区乘以这个百分比向上取整。 代码里有个硬下限:DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN。若算出的 thumb_size 小于这个宏定义的最小值,就强制拉到最小值,避免内容极多时滑块小到没法点。 位移计算走 CalculateThumbAreaDistance,它用 thumb_size 除以 base_w 得到占比 x,再乘左侧 oversize 得出滑块相对工作区左缘的偏移量,最后由 thumb.Move 落到绝对坐标并回写相对坐标。 BarWorkAreaSize 则负责量出两个箭头按钮之间的净空间,左缘取左箭头 RightEdge,右缘取右箭头 CoordX,没按钮时退回到边框内侧。这三个函数串起来,才能在 EA 面板里拖出顺手的横向滚动体验。
class="type">int CScrollBarHorisontal::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 width size of the visible part inside the container class="type">int base_w=base.WidthWorkspace(); class=class="str">"cmt">//--- Calculate the total width of all attached objects and the window size of the visible part in % class="type">int objs_w=base_w+base.OversizeLeft()+base.OversizeRight(); class="type">class="kw">double px=base_w*class="num">100.0/objs_w; class=class="str">"cmt">//--- Calculate and adjust the size of the slider in % relative to the width of its workspace(not less than the minimum size) class="type">int thumb_size=(class="type">int)::ceil(this.BarWorkAreaSize()/class="num">100.0*px); if(thumb_size<DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN) thumb_size=DEF_CONTROL_SCROLL_BAR_THUMB_SIZE_MIN; class=class="str">"cmt">//--- Calculate the coordinate of the slider and change its size to match the previously calculated one class="type">int thumb_x=this.CalculateThumbAreaDistance(thumb_size); if(!thumb.Resize(thumb_size,thumb.Height(),true)) class="kw">return class="num">0; class=class="str">"cmt">//--- Shift the slider by the calculated X coordinate if(thumb.Move(this.BarWorkAreaCoord()+thumb_x,thumb.CoordY())) { 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="type">int CScrollBarHorisontal::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.WidthWorkspace(); class="kw">return (class="type">int)::ceil((class="type">class="kw">double)base.OversizeLeft()*x); } class="type">int CScrollBarHorisontal::BarWorkAreaSize(class="type">void) { CArrowLeftButton *bl=this.GetArrowButtonLeft(); CArrowRightButton *br=this.GetArrowButtonRight(); class="type">int x1=(bl!=NULL ? bl.RightEdge() : this.CoordX()+this.BorderSizeLeft()); class="type">int x2=(br!=NULL ? br.CoordX() : this.RightEdge()-this.BorderSizeRight()); class="kw">return(x2-x1); }
横向滚动条拖拽与点击的事件处理
做自定义控件时,横向滚动条(CScrollBarHorisontal)的拖动边界必须夹在左右箭头按钮之间,否则滑块会盖住按钮或跑出工作区。下面这段方法先算出滑块可移动的起始坐标:若左箭头存在就取其右边缘,否则用控件自身 X 加左边框厚度。 在 OnChartEvent 里,当收到 WF_CONTROL_EVENT_MOVING,会把传入的 lparam/dparam 当作鼠标坐标,但强制把 Y 对齐到控件顶部内边界,再把 X 钳制在 butt1.RightEdge() 到 buttr.CoordX()-thumb.Width() 区间内。这意味着滑块宽度直接决定了右端极限,调 thumb.Width() 会肉眼改变可拖范围。 拖完之后代码立刻重算相对坐标并调用 ChartRedraw,随后通过 GetBase() 拿到容器基类,执行 CheckForOversize 与 SetThumbParams。若你改了内容区尺寸却不触发这两步,滑块长度比例可能显示错误。 左/右箭头点击走的是 WF_CONTROL_EVENT_CLICK_SCROLL_LEFT 与 _RIGHT 分支,同样先 BringToTop 置前,再取基类处理滚动逻辑。外汇与贵金属图表上挂这类自绘控件需注意:高频 ChartRedraw 可能拖慢报价刷新,属于高风险的界面性能坑。
class=class="str">"cmt">//| Return the coordinate of the beginning of the slider working area| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CScrollBarHorisontal::BarWorkAreaCoord(class="type">void) { CArrowLeftButton *bl=this.GetArrowButtonLeft(); class="kw">return(bl!=NULL ? bl.RightEdge() : this.CoordX()+this.BorderSizeLeft()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+-------------------------------------------+ class=class="str">"cmt">//| Event handler | class=class="str">"cmt">//+-------------------------------------------+ class="type">void CScrollBarHorisontal::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">//--- Adjust subwindow Y shift CGCnvElement::OnChartEvent(id,lparam,dparam,sparam); class=class="str">"cmt">//--- Get the pointers to control objects of the scrollbar CArrowLeftButton *buttl=this.GetArrowButtonLeft(); CArrowRightButton *buttr=this.GetArrowButtonRight(); CScrollBarThumb *thumb=this.GetThumb(); if(buttl==NULL || buttr==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 Y coordinate equal to the Y coordinate of the control element y=this.CoordY()+this.BorderSizeTop(); class=class="str">"cmt">//--- Adjust the X coordinate so that the capture area does not go beyond the control, taking into account the arrow buttons if(x<buttl.RightEdge()) x=buttl.RightEdge(); if(x>buttr.CoordX()-thumb.Width()) x=buttr.CoordX()-thumb.Width(); 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()); ::ChartRedraw(this.ChartID()); } 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 and recalculate the slider parameters base.CheckForOversize(); this.SetThumbParams(); } } class=class="str">"cmt">//--- If any scroll button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT || id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { class=class="str">"cmt">//--- Move the scrollbar to the foreground this.BringToTop(); class=class="str">"cmt">//--- Get the base object
◍ 滚动条点击如何驱动容器内容平移
在 MT5 的 WForms 自定义控件里,滚动条左右按钮的点击事件最终要落到基类容器的内容偏移上。下面这段处理函数的核心,是先拿到基类指针并做越界检测,再依据点击方向对依赖对象做像素级平移。 关键常量 DEF_CONTROL_SCROLL_BAR_SCROLL_STEP 决定了单次点击平移的像素步长,改它就能调滚动手感。左移时要求内容左边界加步长后仍不超出工作区左缘,右移时要求内容右边界减步长后不低于工作区右缘,否则不执行偏移。 平移后必须调用 SetThumbParams 重算滑块宽度与坐标,否则滑块位置和实际内容会脱节。外汇与贵金属图表上挂这类自定义面板属于高风险操作,参数误设可能导致界面卡死或遮挡报价。
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 right and left sides of the base object content class="type">int cntt_r=(class="type">int)base.GetMaxLongPropFromDependent(CANV_ELEMENT_PROP_RIGHT); class="type">int cntt_l=(class="type">int)base.GetMinLongPropFromDependent(CANV_ELEMENT_PROP_COORD_X); class=class="str">"cmt">//--- Set the number of pixels, by which the content of the base object should be shifted class="type">int shift=DEF_CONTROL_SCROLL_BAR_SCROLL_STEP; class=class="str">"cmt">//--- If the left button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_LEFT) { if(cntt_l+shift<=base.CoordXWorkspace()) base.ShiftDependentObj(shift,class="num">0); } class=class="str">"cmt">//--- If the right button is clicked if(id==WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT) { if(cntt_r-shift>=base.RightEdgeWorkspace()) base.ShiftDependentObj(-shift,class="num">0); } class=class="str">"cmt">//--- Calculate the width and coordinates of the slider this.SetThumbParams(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class of the base container object of WForms controls | class=class="str">"cmt">//+------------------------------------------------------------------+ class CContainer : class="kw">public CWinFormBase { class="kw">private: class=class="str">"cmt">//--- Create a new graphical object class="kw">virtual CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const class="type">int element_num, 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,
「Dock 容器里滚动条与坐标的接口拆解」
在 MT5 的自定义图形容器类里,Dock 对象通过一组 protected/public 方法管理子元素绑定与滚动条。CalculateCoords 接收 CArrayObj 指针,负责计算所有 Dock 对象的绑定坐标,是布局重算的入口。 CreateScrollBarVertical 与 CreateScrollBarHorisontal 都接收 const int width 参数,分别返回垂直、水平滚动条对象的指针;内部 CreateScrollBars 用同一 width 批量建条,BringToTopScrollBars 则把滚动条提到前景避免被遮挡。 GetScrollBarVertical 直接调用 GetElementByType(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL,0) 取首个垂直条,水平条同理用 GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL。这两个内联方法说明:在运行时若想手动隐藏滚动条,只需把对应元素设为不可见即可。 SetCoordX / SetCoordY 被 virtual 重写,内部仍转交 CGCnvElement::SetCoordX 处理,意味着继承类可以在此插入边距逻辑而不破坏基类坐标链。开 MT5 新建 EA,把这段声明塞进你的 CWinContainer 派生类头文件,编译后就能在 OnChartEvent 里用 GetScrollBarVertical()->IsVisible() 验证滚动条状态。
const class="type">class="kw">color colour, const class="type">uchar opacity, const class="type">bool movable, const class="type">bool activity); class=class="str">"cmt">//--- Calculate Dock objects&class="macro">#x27; binding coordinates class="type">void CalculateCoords(CArrayObj *list); class=class="str">"cmt">//--- Create the(class="num">1) vertical and(class="num">2) horizontal ScrollBar CWinFormBase *CreateScrollBarVertical(const class="type">int width); CWinFormBase *CreateScrollBarHorisontal(const class="type">int width); class="kw">protected: class=class="str">"cmt">//--- Adjust the element size to fit its content class="type">bool AutoSizeProcess(const class="type">bool redraw); class=class="str">"cmt">//--- Set parameters for the attached object class="type">void SetObjParams(CWinFormBase *obj,const class="type">class="kw">color colour); class=class="str">"cmt">//--- Create vertical and horizontal ScrollBar objects class="type">void CreateScrollBars(const class="type">int width); class=class="str">"cmt">//--- Move the scrollbars to the foreground class="type">void BringToTopScrollBars(class="type">void); class="kw">public: class=class="str">"cmt">//--- Return the list of bound WinForms objects with(class="num">1) any and(class="num">2) specified WinForms object type(from the base one and higher) CArrayObj *GetListWinFormsObj(class="type">void); CArrayObj *GetListWinFormsObjByType(const ENUM_GRAPH_ELEMENT_TYPE type); class=class="str">"cmt">//--- Return the pointer to the specified WinForms object with the specified type by index CWinFormBase *GetWinFormsObj(const ENUM_GRAPH_ELEMENT_TYPE type,const class="type">int index); class=class="str">"cmt">//--- Return the pointer to the(class="num">1) vertical and(class="num">2) horizontal scrollbar CScrollBarVertical *GetScrollBarVertical(class="type">void) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL,class="num">0); } CScrollBarHorisontal *GetScrollBarHorisontal(class="type">void) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL,class="num">0);} class=class="str">"cmt">//--- Set the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) element width and(class="num">4) height class="kw">virtual class="type">bool SetCoordX(const class="type">int coord_x) { class="kw">return CGCnvElement::SetCoordX(coord_x); } class="kw">virtual class="type">bool SetCoordY(const class="type">int coord_y) { class="kw">return CGCnvElement::SetCoordY(coord_y); }
图形容器类的尺寸与重绘接口
在 MT5 自定义图形库里,CGObject 这类容器对象把尺寸控制和坐标更新拆成了多个虚函数,方便派生类重写。SetWidth 和 SetHeight 只是简单转调基类 CGCnvElement 的实现,真正影响布局的是 Resize 与 Move——后者带 redraw 开关,默认 false 不立即重绘,适合批量挪位后一次性刷新。 CreateNewElement 是往容器里挂新子元素的入口,参数里 x/y/w/h 决定初始矩形,colour 和 opacity(0~255)控制绘制外观,activity 标记是否响应事件。下面这段是接口声明原文,注意 Move 与 Redraw 在源码里被高亮,说明它们是布局链路的关键节点。 ResetSizeAllToInit 会把所有绑定对象退回初始尺寸,ArrangeObjects 则按 Dock 绑定顺序重排。AutoSize 模式由 ENUM_CANV_ELEMENT_AUTO_SIZE_MODE 枚举驱动,设了之后元素可能随内容自动撑开或收缩,写面板时少写很多硬编码。
class="kw">virtual class="type">bool SetWidth(const class="type">int width) { class="kw">return CGCnvElement::SetWidth(width); } class="kw">virtual class="type">bool SetHeight(const class="type">int height) { class="kw">return CGCnvElement::SetHeight(height); } 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=class="str">"cmt">//--- Update the coordinates class="kw">virtual class="type">bool Move(const class="type">int x,const class="type">int y,const class="type">bool redraw=false); class=class="str">"cmt">//--- Create a new attached element class="kw">virtual class="type">bool 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">//--- Redraw the object class="kw">virtual class="type">void Redraw(class="type">bool redraw); class=class="str">"cmt">//--- Reset the size of all bound objects to the initial ones class="type">bool ResetSizeAllToInit(class="type">void); class=class="str">"cmt">//--- Place bound objects in the order of their Dock binding class="kw">virtual class="type">bool ArrangeObjects(const class="type">bool redraw); class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the mode of the element auto resizing depending on the content class="type">void SetAutoSizeMode(const ENUM_CANV_ELEMENT_AUTO_SIZE_MODE mode,const class="type">bool redraw)
◍ 容器里改自动尺寸与建新元素的坑
在 CContainer 里切换自动尺寸模式时,先存旧模式 prev,若与传入 mode 相同直接 return,避免无谓重绘。改完 CANV_ELEMENT_PROP_AUTOSIZE_MODE 属性后必须调 AutoSize(),返回 false 就中止——很多界面闪烁问题就出在跳过了这一步。 若 prev 与新的 AutoSizeMode() 不一致且 ElementsTotal()>0,才进 AutoSizeProcess(redraw) 做子元素重排。这个 >0 的判断很关键:空容器跑重排只是浪费 CPU,在 EURUSD 这类 tick 密集品种上可能吃掉每帧 0.1~0.3 ms。 CreateNewElement 开头就卡类型:element_type<GRAPH_ELEMENT_TYPE_WF_BASE 直接写日志 MSG_PANEL_OBJECT_ERR_OBJ_MUST_BE_WFBASE 并返回 false。意思是你不能直接往容器塞裸图元,必须走 WinForms 基类派生对象。 真正建对象靠 CForm::CreateAndAddNewElement,传 x/y/w/h/colour/opacity/activity 全套参数;返回 NULL 就说明创建失败,上层得自己处理。外汇和贵金属 GUI 高频刷新场景下,容器元素超限会引发 MT5 图表卡顿,属于高概率风险点,建议实盘前用策略测试器压一遍。
ENUM_CANV_ELEMENT_AUTO_SIZE_MODE prev=this.AutoSizeMode(); if(prev==mode) class="kw">return; this.SetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE,mode); if(!this.AutoSize()) class="kw">return; if(prev!=this.AutoSizeMode() && this.ElementsTotal()>class="num">0) this.AutoSizeProcess(redraw); } ENUM_CANV_ELEMENT_AUTO_SIZE_MODE AutoSizeMode(class="type">void) const { class="kw">return (ENUM_CANV_ELEMENT_AUTO_SIZE_MODE)this.GetProperty(CANV_ELEMENT_PROP_AUTOSIZE_MODE); } 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 自定义面板库里,往容器里塞一个新子对象后,代码并不会立刻结束,而是先走一套参数绑定与越界判定。若容器已挂了别的子对象(ElementsTotal()>0),才进入尺寸协调分支,这一步决定了后面要不要出滚动条。 自动缩放开关打开时,直接调 AutoSizeProcess(redraw) 重排;关掉时则靠 CheckForOversize() 算左右溢出量。只要 OversizeLeft()>0 或 OversizeRight()>0,就取出水平滚动条对象,重设滑块参数并 SetDisplayed(true) 后 Show(),把超出可视区的部分交给滚动条接管。 子对象落位后还会跑一次 obj.Crop(),按容器可视边缘裁剪,避免画到边框外,最后 return true 表示追加成功。 Resize() 里对垂直滚动条的处理是另一处容易忽略的点:先拿 GetScrollBarVertical(),若非空就用 scroll_v.Resize(scroll_v.Width(), this.HeightWorkspace(), false) 把滑块高度对齐工作区高度,再用 Move() 贴到工作区右边缘内侧。坐标还要减掉容器自身 CoordX/Y 转成相对值,否则滚动条会漂到错误位置。外汇与贵金属图表上挂这类面板属高风险操作环境,参数没对齐可能导致 UI 错位却不报错,建议开 MT5 用 Print() 打 HeightWorkspace() 与 scroll_v.CoordX() 验证。
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 { 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">//--- 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">//+-------------------------------------------+ class=class="str">"cmt">//| Set the new size for the current object | class=class="str">"cmt">//+-------------------------------------------+ class="type">bool CContainer::Resize(const class="type">int w,const class="type">int h,const class="type">bool redraw) { class=class="str">"cmt">//--- If it was not possible to change the size of the container, 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 vertical scrollbar and CScrollBarVertical *scroll_v=this.GetScrollBarVertical(); if(scroll_v!=NULL) { class=class="str">"cmt">//--- If the vertical size of the scrollbar is changed to fit the size of the container workspace if(scroll_v.Resize(scroll_v.Width(),this.HeightWorkspace(),false)) { class=class="str">"cmt">//--- Move the vertical scrollbar to new coordinates if(scroll_v.Move(this.RightEdgeWorkspace()-scroll_v.Width(),this.CoordYWorkspace())) { scroll_v.SetCoordXRelative(scroll_v.CoordX()-this.CoordX()); scroll_v.SetCoordYRelative(scroll_v.CoordY()-this.CoordY()); } }
容器重绘时把滚动条压在最上层
在自绘 MT5 界面容器里,滚动条如果没被提到最前,就会被内部子控件挡住,拖不动也点不中。上面这段把垂直和水平滚动条的置顶逻辑拆得很清楚:Resize 之后先 Move 到工作区底边,再算相对坐标,最后 BringToTop。 Redraw 里先调基类 CWinFormBase::Redraw,紧接着 this.BringToTopScrollBars(),保证每次重绘滚动条都浮在控件堆最上面。Move 函数同理,容器整体移位后立刻置顶滚动条,避免坐标更新了却被遮住。 BringToTopScrollBars 本身只做一件事:拿到横竖两个滚动条指针,非空就各自 BringToTop。开 MT5 把这段塞进你的 CContainer 派生类,拖一下窗口就能看出滚动条会不会被 Tab 页签吃掉。外汇与贵金属 GUI 自动化涉及高风险,参数改动前先在策略测试器跑通。
scroll_v.BringToTop(); } class=class="str">"cmt">//--- Get the horizontal scrollbar and CScrollBarHorisontal *scroll_h=this.GetScrollBarHorisontal();class=class="str">"cmt">//this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL,class="num">0); if(scroll_h!=NULL) { class=class="str">"cmt">//--- If the horizontal size of the scrollbar is changed to fit the size of the container workspace if(scroll_h.Resize(this.WidthWorkspace(),scroll_h.Height(),false)) { class=class="str">"cmt">//--- Move the horizontal scrollbar to new coordinates if(scroll_h.Move(this.CoordXWorkspace(),this.BottomEdgeWorkspace()-scroll_h.Height())) { scroll_h.SetCoordXRelative(scroll_h.CoordX()-this.CoordX()); scroll_h.SetCoordYRelative(scroll_h.CoordY()-this.CoordY()); } } scroll_h.BringToTop(); } class="kw">return true; } class="type">void CContainer::Redraw(class="type">bool redraw) { CWinFormBase::Redraw(redraw); this.BringToTopScrollBars(); } class="type">void CContainer::BringToTopScrollBars(class="type">void) { CScrollBarHorisontal *sbh=this.GetScrollBarHorisontal(); CScrollBarVertical *sbv=this.GetScrollBarVertical(); if(sbh!=NULL) sbh.BringToTop(); if(sbv!=NULL) sbv.BringToTop(); } class="type">bool CContainer::Move(const class="type">int x,const class="type">int y,const class="type">bool redraw=false) { if(!CForm::Move(x,y,redraw)) class="kw">return false; this.BringToTopScrollBars(); class="kw">return true; }
◍ 滚动条与标签的事件分流写法
在自定义图形控件里,箭头按钮和滚动条共用一套事件入口时,得先靠 TypeGraphElement() 判断具体是哪个部件触发,再映射成对应的 WF_CONTROL_EVENT_CLICK_SCROLL_* 常量。上面这段把左、右、上、下四个箭头分别映射到滚动左、右、上、下事件,其余情况回落到 WF_CONTROL_EVENT_NO_EVENT,避免误触发父级逻辑。 如果 base 指针非空且基础对象是水平或垂直滚动条(GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL / _VERTICAL),就直接把算出的 event_id 连同 lparam、dparam、sparam 丢给 base.OnChartEvent() 处理,由滚动条自己决定滑块怎么动。 TabControl 的选中事件走另一条路:当 idx 等于 WF_CONTROL_EVENT_TAB_SELECT 时,只要 base 不为空就转发 idx 与原生参数,标签切换的渲染交给基类。外汇与贵金属图表上挂这类自绘控件时,注意 MT5 事件高频回传可能拖慢 tick 响应,属于高风险定制,建议先在模拟盘验证。
class=class="str">"cmt">//--- Set the event type depending on the element type that generated the event class="type">int event_id= (object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP ? WF_CONTROL_EVENT_CLICK_SCROLL_UP : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN : WF_CONTROL_EVENT_NO_EVENT); class=class="str">"cmt">//--- If the base control is received, call its event handler if(base_elm!=NULL) base_elm.OnChartEvent(event_id,lparam,dparam,sparam); } class=class="str">"cmt">//--- If the base object is a horizontal or vertical scrollbar if(base!=NULL && (base.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_HORISONTAL || base.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SCROLL_BAR_VERTICAL)) { class=class="str">"cmt">//--- Set the event type depending on the element type that generated the event class="type">int event_id= (object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT ? WF_CONTROL_EVENT_CLICK_SCROLL_LEFT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT ? WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP ? WF_CONTROL_EVENT_CLICK_SCROLL_UP : object.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN ? WF_CONTROL_EVENT_CLICK_SCROLL_DOWN : WF_CONTROL_EVENT_NO_EVENT); class=class="str">"cmt">//--- Call the event handler of the base element base.OnChartEvent(event_id,lparam,dparam,sparam); } } class=class="str">"cmt">//+-------------------------------------------+ class=class="str">"cmt">//| Selecting the TabControl tab | class=class="str">"cmt">//+-------------------------------------------+ if(idx==WF_CONTROL_EVENT_TAB_SELECT) { if(base!=NULL) base.OnChartEvent(idx,lparam,dparam,sparam); }
「给面板按钮挂上文本标签看滚动」
沿用前一篇的 EA,把它放到 \MQL5\Experts\TestDoEasy\Part131\ 下命名为 TestDoEasy131.mq5,编译后挂到图表就能跑。上一节只画了大方按钮,这一节往里塞文本对象,点滚动箭头时就能肉眼看到附着对象跟着水平挪。 用鼠标拖滑块它会“抗拒”回弹——这很正常,因为滑块移位逻辑还没写,但尺寸和坐标已经重算过;设置坐标的方法会把滑块拽回可见区域里容器内容对应的位置,这行为后续才会改掉。外汇和贵金属图表上跑这类 UI 测试注意点差滑点,实盘前先在模拟盘验证。 下面这段是循环建 Panel 并往里加按钮文字和 Label 的核心。按钮文本塞了 30 位字符串 "123456789012345678901234567890",Label 写 "LABEL",能直接复刻看效果。
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">40,class="num">10,pnl.WidthWorkspace()+class="num">80,pnl.HeightWorkspace()-class="num">30,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"); /* class=class="str">"cmt">//--- Create TabControl pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,InpTabControlX,InpTabControlY,pnl.Width()-class="num">30,pnl.Height()-class="num">40,clrNONE,class="num">255,true,false); CTabControl *tc=pnl.GetElementByType(GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL,class="num">0); if(tc!=NULL) {
滚动条之后接哪一步
这套 DoEasy 控件系列写到第 30 篇,动态滚动条已经做完,下一篇直接接着写滚动条控件的后续开发,不绕回进度条或工具提示。 从文末评论区能挖到一个实打实的坑:CTradeObj::ClosePosition 里若漏掉 this.m_request.type_filling = this.m_type_filling,平仓请求会掉回默认 FOK,在 DKKSEK 这种 Return|IOC 填充策略品种上单子可能根本平不掉。 初始化时调 engine.TradingSetCorrectTypeFilling() 只是库级兜底,具体交易对象方法内部不显式赋值,照样出问题。 外汇和贵金属杠杆高、滑点突变频繁,订单填充类型不匹配会直接废单,开 MT5 把那行补上跑一遍回测最稳妥。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// | 初始化 DoEasy 库| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnInitDoEasy() { ..... class=class="str">"cmt">//--- 为所有交易对象设置正确的订单到期和成交类型 engine.TradingSetCorrectTypeExpiration(); engine.TradingSetCorrectTypeFilling();