DoEasy. 控件 (第 3 部分): 创建绑定控件·综合运用
- 图形对象基类的接口拆解
- 画布对象的坐标与层级接口
- 画布类怎么把动画和图元绑在一起
- 表单内图形对象的实例化分支
- 依附元素的创建与坐标换算
- 给画布表单挂元素的防重入逻辑
- 表单拖动时子对象与阴影的同步搬迁
- 前景层级与面板容器属性
- 控件内边距与图形对象创建的底层约束
- 面板内边距的强制下限逻辑
- 面板基类的对象生成与默认样式
- 面板对象与画布元素检索的实现切口
- 面板对象的构造参数与注册逻辑
- 面板阴影与边框绘制的代码落点
- 用渐变数组画面板背景
- 图形对象属性落盘与阴影绘制
- 图形容器类的成员与取数接口
- 按名称与图表ID抓取画布元素实例
- 按图表与名称捞取窗体对象的三种重载
- 用三种索引拿回面板对象
- 渐变填充对象的参数分支逻辑
- 水平渐变与循环渐变的二选一创建
- 在主图直接落一个 WinForm 图元
- 渐变面板的参数分支逻辑
- 在当前图表子窗口里生成带渐变填充的窗体
- 在当前图表挂一个 WinForm 窗体
- 面板渐变分支的构造参数拆解
- WinForm 面板的渐变与创建分支
- 在主图建 WinForm 面板的重载入口
- 引擎构造里的账户模式嗅探
- 把控件挂到面板上的实跑验证
- 在面板里铺四种渐变控件
- 循环挂接五个表单对象到面板基类
- 面板控件的下一段路
「图形对象基类的接口拆解」
在 MT5 自定义面板开发里,图形元素的父类通常先声明一批受保护方法,用来处理阴影对象与依赖对象的命名。CreateShadowObj 接收颜色和透明度参数,负责在画布背后生成半透明投影;CreateNameDependentObject 则把当前对象名去掉 EA/指标名前缀(用 MQL_PROGRAM_NAME 长度 +1 截断),再拼上自定义后缀,保证多实例下对象名不冲突。 公开接口里最重的是 CreateNewGObject,它用虚函数把矩形坐标(x,y,w,h)、颜色、透明度、是否可拖拽、是否激活全部传进去,返回 CGCnvElement 指针。GetCoords 以引用方式回写绑定对象的初始坐标,MouseFormState 则根据鼠标事件 id 与 lparam 判定光标相对表单的状态并返回坐标。 直接把这套声明拷进你的 .mqh 头文件,在 MT5 里建个空 EA 调用 CreateNameDependentObject,能看到对象名自动带出『_base』这类后缀,验证命名逻辑是否如预期。外汇与贵金属图表上挂这类面板属高风险操作,参数误写可能导致对象堆积拖慢终端。
class=class="str">"cmt">//--- Create a shadow object class="type">void CreateShadowObj(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity); class=class="str">"cmt">//--- Return the name of the dependent object class="type">class="kw">string CreateNameDependentObject(class="kw">const class="type">class="kw">string base_name) class="kw">const { class="kw">return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+class="num">1)+"_"+base_name; } class="kw">public: class=class="str">"cmt">//--- Create a new graphical object class="kw">virtual CGCnvElement *CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int element_num, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity); class=class="str">"cmt">//--- Return the initial coordinates of a bound object class="kw">virtual class="type">void GetCoords(class="type">int &x,class="type">int &y); class="kw">public: class=class="str">"cmt">//--- Return(class="num">1) the mouse status relative to the form, as well as(class="num">2) X and(class="num">3) Y coordinate of the cursor ENUM_MOUSE_FORM_STATE MouseFormState(class="kw">const class="type">int id,class="kw">const class="type">long lparam,class="kw">const class="type">class="kw">double dparam,class="kw">const class="type">class="kw">string sparam);
画布对象的坐标与层级接口
在自定义图形面板类里,鼠标位置和画布偏移是分开管理的。MouseCursorX / MouseCursorY 直接返回内部 m_mouse 的像素坐标,而 SetOffsetX / OffsetX 这组接口控制的是画布相对光标的固定位移,默认偏移量为 0,改了之后所有子对象会跟着整体平移。 Move(x, y, redraw) 是虚函数,负责把整块画布搬到新坐标,redraw 默认 false 不重绘,需要立刻可见就传 true。SetZorder 和 BringToTop 管点击命中优先级,Zorder 数值大的对象倾向盖在前面,BringToTop 则强制置顶,做浮动窗口时这两个不能漏。 GetObject 返回自身指针,GetListElements 拿出挂载元素数组,GetShadowObj 给到底层阴影对象指针。要在 MT5 里验证,把这些方法挂到自己的 CForm 派生类,打印 OffsetX 改前改后的值就能看到画布位移效果。
class="type">int MouseCursorX(class="type">void) class="kw">const { class="kw">return this.m_mouse.CoordX(); } class="type">int MouseCursorY(class="type">void) class="kw">const { class="kw">return this.m_mouse.CoordY(); } class=class="str">"cmt">//--- Set the flags of mouse scrolling, context menu and the crosshairs tool for the chart class="type">void SetChartTools(class="kw">const class="type">bool flag); class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the shift of X and Y coordinates relative to the cursor class="type">void SetOffsetX(class="kw">const class="type">int value) { this.m_offset_x=value; } class="type">void SetOffsetY(class="kw">const class="type">int value) { this.m_offset_y=value; } class="type">int OffsetX(class="type">void) class="kw">const { class="kw">return this.m_offset_x; } class="type">int OffsetY(class="type">void) class="kw">const { class="kw">return this.m_offset_y; } class=class="str">"cmt">//--- Update the coordinates(shift the canvas) class="kw">virtual class="type">bool Move(class="kw">const class="type">int x,class="kw">const class="type">int y,class="kw">const class="type">bool redraw=class="kw">false); class=class="str">"cmt">//--- Set the priority of a graphical object for receiving the event of clicking on a chart class="kw">virtual class="type">bool SetZorder(class="kw">const class="type">long value,class="kw">const class="type">bool only_prop); class=class="str">"cmt">//--- Set the object above all class="kw">virtual class="type">void BringToTop(class="type">void); class=class="str">"cmt">//--- Event handler class=class="str">"cmt">//--- Return(class="num">1) the list of attached objects and(class="num">2) the shadow object CForm *GetObject(class="type">void) { class="kw">return &this; } CArrayObj *GetListElements(class="type">void) { class="kw">return &this.m_list_elements; } CShadowObj *GetShadowObj(class="type">void) { class="kw">return this.m_shadow_obj; }
◍ 画布类怎么把动画和图元绑在一起
在 MT5 自定义图形界面里,容器类负责把动画对象、文本帧、矩形帧以及外部图元统一管理。下面这段接口声明展示了它向外暴露的存取方式,直接决定了你在 EA 里该如何拿到这些指针去重绘。 GetAnimationsObj() 返回动画对象指针;GetListFramesText() 与 GetListFramesQuad() 分别在 m_animations 非空时返回文本帧列表和矩形帧列表,空则回 NULL。这意味着调用前必须确认动画对象已创建,否则拿到空指针去遍历会直接崩。 ElementsTotal() 返回 m_list_elements.Total(),即当前绑定的图元数量;GetElement(index) 按索引取回 CGCnvElement*。这两个方法是你做逐元素刷新(比如根据金价跳动重画提示框)的入口。 CreateNewElement() 用坐标 x,y、宽高 w,h、颜色 colour、透明度 opacity、是否可交互 activity 来新建一个附属图元;AddNewElement() 则把一个已存在的 CGCnvElement 指针塞进列表并定位。外汇与贵金属图表上叠加这类自绘控件属于高风险操作环境,参数填错可能导致图表资源泄漏。 把上面接口复制到你的画布头文件里,开 MT5 写个最小 EA 循环调用 ElementsTotal() 打印到日志,就能验证绑定数量是否随 CreateNewElement 调用递增。
class=class="str">"cmt">//--- Return the pointer to(class="num">1) the animation object, the list of(class="num">2) text and(class="num">3) rectangular animation frames CAnimations *GetAnimationsObj(class="type">void) { class="kw">return this.m_animations; } CArrayObj *GetListFramesText(class="type">void) { class="kw">return(this.m_animations!=NULL ? this.m_animations.GetListFramesText() : NULL); } CArrayObj *GetListFramesQuad(class="type">void) { class="kw">return(this.m_animations!=NULL ? this.m_animations.GetListFramesQuad() : NULL); } class=class="str">"cmt">//--- Return the(class="num">1) number of bound elements and(class="num">2) the bound element by the index in the list class="type">int ElementsTotal(class="type">void) class="kw">const { class="kw">return this.m_list_elements.Total(); } CGCnvElement *GetElement(class="kw">const class="type">int index) { class="kw">return this.m_list_elements.At(index); } class=class="str">"cmt">//--- Set the form(class="num">1) class="type">class="kw">color scheme and(class="num">2) style class=class="str">"cmt">//--- Create a new attached element class="type">bool CreateNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool activity); class=class="str">"cmt">//--- Add a new attached element class=class="str">"cmt">//--- Add a new attached element class="type">bool AddNewElement(CGCnvElement *obj,class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- Draw an object shadow
「表单内图形对象的实例化分支」
在自定义 UI 框架里,CForm::CreateNewGObject 负责按类型把画布元素真正 new 出来。它先通过 CreateNameDependentObject 给对象拼出依赖父级的唯一名,避免同图表多实例重名冲突。 核心分发逻辑是一个 switch(type):当类型为 GRAPH_ELEMENT_TYPE_ELEMENT 时,直接构造 CGCnvElement,把图表 ID、子窗口、坐标宽高、颜色与透明度一股脑传进去;当类型为 GRAPH_ELEMENT_TYPE_FORM 时,则 new 一个 CForm 子窗体,此时只传图表 ID、子窗口、名称与几何参数。 注意 DrawShadow 的默认参:opacity=127 意味着半透明阴影,blur 取宏 DEF_SHADOW_BLUR 控制羽化半径。外汇与贵金属图表上叠加这类自绘 UI 存在刷新开销,过高透明度叠加层数多时可能拖慢 MT5 的 OnChartEvent 响应。 开 MT5 把这段分支抄进你的面板基类,改 GRAPH_ELEMENT_TYPE_FORM 分支补完构造函数,就能验证子表单是否按预期挂载到父画布坐标 (x,y)。
class="type">void DrawShadow(class="kw">const class="type">int shift_x,class="kw">const class="type">int shift_y,class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity=class="num">127,class="kw">const class="type">uchar blur=DEF_SHADOW_BLUR); class=class="str">"cmt">//--- Draw the form frame class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CForm::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int obj_num, class="kw">const class="type">class="kw">string obj_name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity) { class="type">class="kw">string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; class=class="str">"cmt">//--- Depending on the created object type, class="kw">switch(type) { class=class="str">"cmt">//--- create a graphical element object case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); break; class=class="str">"cmt">//--- create a form object case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
依附元素的创建与坐标换算
在 MT5 自定义图形面板里,依附元素(attached element)不是独立 OBJ_LABEL,而是挂在 CForm 基类坐标系统下的子对象。CForm::CreateNewElement 负责按类型、尺寸、颜色与透明度生成这类元素,并自动写入元素列表。 函数开头先拦截非法类型:若 element_type 小于 GRAPH_ELEMENT_TYPE_ELEMENT(枚举值 0),直接 Print 报错并返回 false,避免把非图形基元当可依附对象处理。 元素命名用列表总数+1 做序号,不足两位补零,例如第 3 个对象得到 "Elm03"。坐标先取入参 x/y,再经 this.GetCoords() 换算成相对基对象的绝对画布坐标,这一步决定了元素在面板里的真实落点。 创建成功后调 AddNewElement 挂进列表;若挂接失败则 delete 指针并返回 false,防止野指针残留。最后写回基准对象、相对坐标与 Z 序,并执行 Erase 重绘。外汇与贵金属图表上跑这类 UI 逻辑属高风险环境,参数错了可能整块面板不显示,建议开 MT5 用 Print 跟踪 num 与 elm_x/elm_y 验证。
class="type">bool CForm::CreateNewElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool activity) { class=class="str">"cmt">//--- If the type of a created graphical element is less than the "element", inform of that and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27; if(element_type<GRAPH_ELEMENT_TYPE_ELEMENT) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_NOT_INTENDED),::StringSubstr(::EnumToString(element_type),class="num">19)); class="kw">return class="kw">false; } class=class="str">"cmt">//--- Specify the element index in the list class="type">int num=this.m_list_elements.Total()+class="num">1; class=class="str">"cmt">//--- Create a graphical element name class="type">class="kw">string ns=(::StringLen((class="type">class="kw">string)num)<class="num">2 ? ::IntegerToString(num,class="num">2,&class="macro">#x27;class="num">0&class="macro">#x27;) : (class="type">class="kw">string)num); class="type">class="kw">string name="Elm"+ns; class=class="str">"cmt">//--- Get the initial coordinates of the object relative to the coordinate system of the base object class="type">int elm_x=x; class="type">int elm_y=y; this.GetCoords(elm_x,elm_y); class=class="str">"cmt">//--- Create a new graphical element CGCnvElement *obj=this.CreateNewGObject(element_type,num,name,elm_x,elm_y,w,h,colour,opacity,class="kw">false,activity); if(obj==NULL) class="kw">return class="kw">false; class=class="str">"cmt">//--- and add it to the list of bound graphical elements if(!this.AddNewElement(obj,elm_x,elm_y)) { class="kw">delete obj; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Set the minimum properties for a bound graphical element obj.SetBase(this.GetObject()); obj.SetCoordXRelative(x); obj.SetCoordYRelative(y); obj.SetZorder(this.Zorder(),class="kw">false); class=class="str">"cmt">//--- Draw an added object and class="kw">return &class="macro">#x27;true&class="macro">#x27; obj.Erase(colour,opacity,true);
◍ 给画布表单挂元素的防重入逻辑
在 MT5 自定义图形界面里,CForm::AddNewElement 负责把一个画布元素挂到表单的元素链表上。它先判空,再遍历 m_list_elements 里已有节点,用 elm.Name() 和待加入对象的 Name() 做字符串比对,名字撞车就打印 'OBJ_ALREADY_IN_LIST' 并回 false,避免同一对象被重复绑定导致重绘异常。 下面这段是高亮区的核心防重代码,注意它先排序再二分查找,比纯线性遍历更稳: this.m_list_elements.Sort(SORT_BY_CANV_ELEMENT_NAME_OBJ); int index=this.m_list_elements.Search(obj); if(index>WRONG_VALUE) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj()); return false; } Sort 按对象名排序后,Search 返回下标;只要大于 WRONG_VALUE(通常为 -1)就说明对象已在表里。实盘 EA 面板若频繁动态增删控件,这套判断能把『重复 Add 导致坐标错乱』的概率压下去。 GetCoords 则把表单左上角坐标叠加左边框、上边框宽度,回传出元素在画布上的绝对像素位置;Move 里若 Movable() 为 false 且 base 为空会直接返回 false,意味着锁死的静态表单不会被误拖。外汇与贵金属图表上跑这类 UI 需注意:图形对象交互本身不影响报价,但 MT5 图形资源占用过高可能拖慢 tick 处理,属低风险伴随高波动环境。
this.m_list_elements.Sort(SORT_BY_CANV_ELEMENT_NAME_OBJ); class="type">int index=this.m_list_elements.Search(obj); if(index>WRONG_VALUE) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_OBJ_ALREADY_IN_LIST),": ",obj.NameObj()); class="kw">return class="kw">false; }
「表单拖动时子对象与阴影的同步搬迁」
在自定义面板(CForm)里,Move 不只挪主框,还要把阴影和所有绑定子元素一起带走。先看一段处理阴影与主坐标的逻辑:若阴影对象为空或位移失败,直接返回 false,避免半吊子重绘。
| [CODE]if(this.m_shadow_obj==NULL | !this.m_shadow_obj.Move(x-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetX(),y-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetY(),false)) |
|---|
return false; } //--- If failed to set new values into graphical object properties, return 'false'
| if(!this.SetCoordX(x) | !this.SetCoordY(y)) |
|---|
return false; //--- In the loop by all bound objects, for(int i=0;i<this.m_list_elements.Total();i++) { //--- get the next object and shift it CGCnvElement *obj=m_list_elements.At(i); if(obj==NULL) continue; if(!obj.Move(x+this.m_frame_width_left+obj.CoordXRelative(),y+this.m_frame_width_top+obj.CoordYRelative(),false)) return false; } //--- If the update flag is set and this is a base object, redraw the chart. if(redraw && base==NULL) ::ChartRedraw(this.ChartID()); //--- Return 'true' return true; }[/CODE] 逐行拆一下:第1行判断阴影对象是否存在,并用 Move 把阴影偏移到主坐标左上方 OUTER_AREA_SIZE 处(叠加阴影自身 Offset);第4行写主框 X/Y 坐标,任一失败就退出;for 循环从 m_list_elements 取子元素,按相对坐标 + 边框宽度算绝对位置后 Move,任一失败返回 false;最后当 redraw 且 base 为空才 ChartRedraw,否则交由上层统一刷。 SetZorder 也是同样套路:先设自身层级,再循环给阴影和全部子对象设相同 value。循环里用 total 缓存了列表长度,少一次 Total() 调用。外汇和贵金属图表上挂这类面板时,高频拖动可能触发多次 ChartRedraw,注意 base 参数传递,否则有概率造成画面闪烁。
if(this.m_shadow_obj==NULL || !this.m_shadow_obj.Move(x-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetX(),y-OUTER_AREA_SIZE+this.m_shadow_obj.OffsetY(),class="kw">false)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- If failed to set new values into graphical object properties, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27; if(!this.SetCoordX(x) || !this.SetCoordY(y)) class="kw">return class="kw">false; class=class="str">"cmt">//--- In the loop by all bound objects, for(class="type">int i=class="num">0;i<this.m_list_elements.Total();i++) { class=class="str">"cmt">//--- get the next object and shift it CGCnvElement *obj=m_list_elements.At(i); if(obj==NULL) class="kw">continue; if(!obj.Move(x+this.m_frame_width_left+obj.CoordXRelative(),y+this.m_frame_width_top+obj.CoordYRelative(),class="kw">false)) class="kw">return class="kw">false; } class=class="str">"cmt">//--- If the update flag is set and this is a base object, redraw the chart. if(redraw && base==NULL) ::ChartRedraw(this.ChartID()); class=class="str">"cmt">//--- Return &class="macro">#x27;true&class="macro">#x27; class="kw">return true; }
前景层级与面板容器属性
在自定义图形控件里,把对象推到最前层不是一句 BringToTop 就完事。若控件带阴影对象(m_shadow_obj),要先判断非空再把阴影置顶,随后调用基类 CGCnvElement::BringToTop() 让本体压在阴影之上,最后遍历 m_list_elements 绑定列表,把每个子对象依次置顶,保证嵌套层级不串位。 面板类 CPanel 直接继承 CForm,它管的是整块容器的默认表现。私有成员里 m_fore_color 定下所有子对象文字的默认色,m_bold_type 控制字体宽度类型,m_border_style 决定外框画法——这三个量在初始化时设错,后面逐个控件改样式会很麻烦。 自动滚动与自适应尺寸是实盘挂单面板常用的。m_autoscroll 开启后,m_autoscroll_margin[2] 给出滚动留白;m_autosize 配合 m_autosize_mode 让容器按内容撑开;m_dock_mode 绑定边到父容器,m_margin[4] 则存四边间距。外汇与贵金属图表上叠这类 UI 属高风险操作环境,参数乱绑可能造成遮挡报价,建议开 MT5 在测试 EA 里改这几个成员值看重绘差异。
if(this.m_shadow) { class=class="str">"cmt">//--- If the shadow object is created, move it to the foreground if(this.m_shadow_obj!=NULL) this.m_shadow_obj.BringToTop(); } class=class="str">"cmt">//--- Move the object to the foreground(the object is located above the shadow) CGCnvElement::BringToTop(); class=class="str">"cmt">//--- In the loop by all bound objects, class="type">int total=this.m_list_elements.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the next object from the list CGCnvElement *obj=this.m_list_elements.At(i); if(obj==NULL) class="kw">continue; class=class="str">"cmt">//--- and move it to the foreground obj.BringToTop(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Panel object class of WForms controls | class=class="str">"cmt">//+------------------------------------------------------------------+ class CPanel : class="kw">public CForm { class="kw">private: class="type">class="kw">color m_fore_color; class=class="str">"cmt">// Default text class="type">class="kw">color for all panel objects ENUM_FW_TYPE m_bold_type; class=class="str">"cmt">// Font width type ENUM_FRAME_STYLE m_border_style; class=class="str">"cmt">// Panel frame style class="type">bool m_autoscroll; class=class="str">"cmt">// Auto scrollbar flag class="type">int m_autoscroll_margin[class="num">2]; class=class="str">"cmt">// Array of fields around the control during an auto scroll class="type">bool m_autosize; class=class="str">"cmt">// Flag of the element auto resizing depending on the content ENUM_CANV_ELEMENT_AUTO_SIZE_MODE m_autosize_mode; class=class="str">"cmt">// Mode of the element auto resizing depending on the content ENUM_CANV_ELEMENT_DOCK_MODE m_dock_mode; class=class="str">"cmt">// Mode of binding element borders to the container class="type">int m_margin[class="num">4]; class=class="str">"cmt">// Array of gaps of all sides between the fields of the current and adjacent controls
◍ 控件内边距与图形对象创建的底层约束
在 MQL5 自定义图形控件里,内边距不是随便设的。PaddingLeft 的写入逻辑显示:若传入的 value 小于左框宽度 m_frame_width_left,实际落地的 m_padding[0] 会被强制拉到框宽值,保证内容不会被边框吃掉。 CreateNewGObject 是虚函数,参数表一口气吞了类型、序号、名称、x/y/w/h、颜色、透明度、可移动与可激活共 11 个量。这意味着子类重写时,想加默认行为(比如自动按 DPI 缩放)必须原样接住这套签名,否则绑定对象的坐标会错位。 GetCoords 用引用回写初始坐标,配合 m_padding[4] 数组,能在控件重绘时把子元素推到正确位置。实盘加载这类面板做贵金属挂单界面时,外汇与贵金属波动大、点差跳变频繁,建议先在策略测试器里改几组内边距看重绘是否抖动,再上真仓。
class="type">int m_padding[class="num">4]; class=class="str">"cmt">// Array of gaps of all sides inside controls class=class="str">"cmt">//--- Return the font flags class="type">uint GetFontFlags(class="type">void); class=class="str">"cmt">//--- Create a new graphical object class="kw">virtual CGCnvElement *CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int element_num, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity); class=class="str">"cmt">//--- Return the initial coordinates of a bound object class="kw">virtual class="type">void GetCoords(class="type">int &x,class="type">int &y); class="kw">public: class=class="str">"cmt">//--- Set the gap(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right, (class="num">4) at the bottom and(class="num">5) on all sides inside the control class="type">void PaddingLeft(class="kw">const class="type">uint value) { this.m_padding[class="num">0]=((class="type">int)value<this.m_frame_width_left ? this.m_frame_width_left : (class="type">int)value); }
「面板内边距的强制下限逻辑」
在自定义面板控件里,PaddingTop、PaddingRight、PaddingBottom 这几个 setter 并非直接赋值,而是拿传入值与对应边框宽度做比较,取较大者写入 m_padding 数组。 也就是说,若你传 2 而 m_frame_width_top 是 5,实际内边距会被钳在 5,不会比边框更窄。这一机制保证了控件内部字段与边框之间始终有最小间隔,避免绘制重叠。 PaddingAll 则是依次调用四个方向 setter 的统一入口,方便一键设等距。下方代码逐行对应了上述行为,开 MT5 把这段塞进你的 CPanel 派生类即可验证。 外汇与贵金属图表上叠加这类自绘面板属于高风险操作,参数误设可能导致视觉遮挡报价区,建议先在模拟盘图表试跑。
class="type">void PaddingTop(class="kw">const class="type">uint value) { this.m_padding[class="num">1]=((class="type">int)value<this.m_frame_width_top ? this.m_frame_width_top : (class="type">int)value); } class="type">void PaddingRight(class="kw">const class="type">uint value) { this.m_padding[class="num">2]=((class="type">int)value<this.m_frame_width_right ? this.m_frame_width_right : (class="type">int)value); } class="type">void PaddingBottom(class="kw">const class="type">uint value) { this.m_padding[class="num">3]=((class="type">int)value<this.m_frame_width_bottom ? this.m_frame_width_bottom : (class="type">int)value); } class="type">void PaddingAll(class="kw">const class="type">uint value) { this.PaddingLeft(value); this.PaddingTop(value); this.PaddingRight(value); this.PaddingBottom(value); } class=class="str">"cmt">//--- Return the gap(class="num">1) to the left, (class="num">2) at the top, (class="num">3) to the right and(class="num">4) at the bottom between the fields inside the control CPanel(class="kw">const class="type">class="kw">string name) : CForm(::ChartID(),class="num">0,name,class="num">0,class="num">0,class="num">0,class="num">0) { CGBaseObj::SetTypeElement(GRAPH_ELEMENT_TYPE_PANEL); CGCnvElement::SetProperty(CANV_ELEMENT_PROP_TYPE,GRAPH_ELEMENT_TYPE_PANEL); this.m_type=OBJECT_DE_TYPE_GWF_PANEL;
面板基类的对象生成与默认样式
在自定义图形面板里,构造函数收尾通常会把前景色、字重、边距一次性设好:m_fore_color 取默认前景色,m_bold_type 设成 FW_TYPE_NORMAL,MarginAll(3) 留 3 像素外边距,PaddingAll(0) 内部不补白,最后调 Initialize() 完成内部状态初始化。 真正往面板里塞图形元素靠的是 CreateNewGObject()。它按 ENUM_GRAPH_ELEMENT_TYPE 分支 new 出对应对象:GRAPH_ELEMENT_TYPE_ELEMENT 走 CGCnvElement,GRAPH_ELEMENT_TYPE_FORM 走 CForm,坐标宽高、颜色、透明度、是否可拖拽与是否激活都从入参透传。 对象名不是直接用的,而是先过 CreateNameDependentObject(obj_name) 拼出依赖面板 ID 的唯一名,避免 MT5 同一图表上多个面板实例重名覆盖。 开 MT5 新建个 CPanel 派生类,把 MarginAll(3) 改成 5,看子图边缘留白变化就能验证这套默认样式确实在构造期生效。
this.m_fore_color=CLR_DEF_FORE_COLOR; this.m_bold_type=FW_TYPE_NORMAL; this.MarginAll(class="num">3); this.PaddingAll(class="num">0); this.Initialize(); class=class="str">"cmt">//--- Destructor class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CPanel::CreateNewGObject(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">int obj_num, class="kw">const class="type">class="kw">string obj_name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color colour, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity) { class="type">class="kw">string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=NULL; class="kw">switch(type) { case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),name,x,y,w,h);
◍ 面板对象与画布元素检索的实现切口
在 MT5 自定义图形库里,GRAPH_ELEMENT_TYPE_PANEL 分支直接 new 一个 CPanel 对象,把图表 ID、子窗口号、name 以及 x/y/w/h 五个参数传进去;若 element 返回 NULL,则通过 ::Print 打出失败信息并带出 name,方便定位是哪一个面板没建出来。 CPanel::GetCoords 做了一件容易忽略的事:它把元素自身坐标 CoordX()/CoordY() 再加上左边框宽 FrameWidthLeft() 和顶边框宽 FrameWidthTop(),才得到画布内的绝对坐标。这意味着你若手动算子控件位置,漏掉边框宽度就会偏出几像素。 检索已建画布元素时有三种入口:按类型用 GetListCanvElementByType 走 CSelect 筛选 CANV_ELEMENT_PROP_TYPE;按 chart_id+name 用 GetListCanvElementByName,取 list.At(0);按 chart_id+element_id 用 GetListCanvElementByID,同样取首节点。list 为 NULL 时两个重载都返回 NULL,调用前必须判空。 开 MT5 把这段挂到自己的 GUI 基类里,改一下 FrameWidthLeft/Top 的返回值,就能直观看到面板内嵌控件整体偏移的变化。外汇与贵金属图表上叠加自绘面板属于高风险操作,参数误用可能导致图形层遮挡报价。
break; case GRAPH_ELEMENT_TYPE_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),name,x,y,w,h); break; class="kw">default: break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); class="kw">return element; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the initial coordinates of a bound object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CPanel::GetCoords(class="type">int &x,class="type">int &y) { x=this.CoordX()+this.FrameWidthLeft()+x; y=this.CoordY()+this.FrameWidthTop()+y; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Return the list of graphical elements by element type CArrayObj *GetListCanvElementByType(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type) { class="kw">return CSelect::ByGraphCanvElementProperty(this.GetListCanvElm(),CANV_ELEMENT_PROP_TYPE,type,EQUAL); } class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- Return the graphical element by chart ID and name CGCnvElement *GetCanvElement(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { CArrayObj *list=this.GetListCanvElementByName(chart_id,name); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the graphical element by chart and object IDs CGCnvElement *GetCanvElement(class="kw">const class="type">long chart_id,class="kw">const class="type">int element_id) { CArrayObj *list=this.GetListCanvElementByID(chart_id,element_id); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Constructor CGraphElementsCollection(); class=class="str">"cmt">//--- Create graphical object WinForms Panel object on canvas on a specified chart and subwindow class="type">int CreatePanel(class="kw">const class="type">long chart_id,
「面板对象的构造参数与注册逻辑」
在 MT5 自定义图形界面里,创建一个可交互面板靠的是一长串带默认值的参数。下面这段函数签名定义了面板的位置、尺寸、颜色、透明度以及是否可移动、是否激活,还允许指定边框样式与阴影。 frame_width 默认填 -1 表示不画额外边框,frame_style 默认 FRAME_STYLE_BEVEL 即斜角浮雕,shadow 与 redraw 默认均为 false,意味着初始不投影、不强制重绘。 函数体先取当前集合总数作为新对象 id,用 new 实例化 CPanel 并丢进统一管理列表。若 AddOrGetCanvElmToCollection 返回错误码,直接返回 WRONG_VALUE 中断;否则把 id 与活动、可移动、背景色、边框色逐一写进对象。 开 MT5 把这段抄进 EA 的界面类,改 clr 和 movable 就能直观看到面板拖拽与配色变化,外汇与贵金属图表叠加此类控件需注意点击遮挡带来的误触高风险。
class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="kw">const class="type">class="kw">color clr, class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity, class="kw">const class="type">int frame_width=-class="num">1, ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="type">int id=this.m_list_all_canv_elm_obj.Total(); CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR) class="kw">return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr); obj.SetColorFrame(clr);
面板阴影与边框绘制的代码落点
在自定义面板的绘制流程里,先通过 SetOpacity 设定整体不透明度并关闭重绘,再用 SetShadow 打开阴影开关。若 shadow 为真,阴影色不是随便取的:把图表背景色先降饱和至单色(饱和度 -100),再压暗 20 个单位,得到 clrS。 阴影本身用 DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR) 画出,偏移量为右下各 3 像素,模糊半径固定为 4。这一套下来,面板在 MT5 画布上会呈现轻微悬浮感,而不是死贴背景。 随后 DrawRectangle 按宽高减 1 描出外框,Erase 做底色清除;当 frame_width 大于 0 时才统一设边框宽度,并用 SetActiveAreaShift 把可交互区向内收。最后 DrawFormFrame 按四边宽度与样式画框,Done 收尾并返回对象 ID。外汇与贵金属图表加载此类自定义图形时波动剧烈,高杠杆下误触参数可能导致界面卡顿,请先在模拟盘验证。
obj.SetOpacity(opacity,class="kw">false); class=class="str">"cmt">//--- Draw the shadow drawing flag obj.SetShadow(shadow); if(shadow) { class=class="str">"cmt">//--- Calculate the shadow class="type">class="kw">color as the chart background class="type">class="kw">color converted to the monochrome one class=class="str">"cmt">//--- and darken the monochrome class="type">class="kw">color by class="num">20 units class="type">class="kw">color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-class="num">100),-class="num">20); class=class="str">"cmt">//--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes class=class="str">"cmt">//--- Set the shadow opacity to the class="kw">default value, while the blur radius is equal to class="num">4 obj.DrawShadow(class="num">3,class="num">3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR); } obj.DrawRectangle(class="num">0,class="num">0,obj.Width()-class="num">1,obj.Height()-class="num">1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,redraw); if(frame_width>class="num">0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style); obj.Done(); class="kw">return obj.ID(); class=class="str">"cmt">//--- Create a WinForms Panel object graphical object on canvas on a specified chart and subwindow with the vertical gradient filling
◍ 用渐变数组画面板背景
在 MT5 自定义面板里做竖向渐变背景,核心是把颜色数组按行映射到画布高度。下面这个函数把 chart_id、subwindow、name 以及 x/y/w/h 定位参数收齐,再吃一个 color &clr[] 引用数组和 opacity 透明度,返回一个面板对象句柄。 调用时 id 直接取 m_list_all_canv_elm_obj.Total(),也就是当前集合里已有元素总数,保证新面板不会和旧对象撞号。new 出来的 CPanel 交给 AddOrGetCanvElmToCollection,若返回 ADD_OBJ_RET_CODE_ERROR 就说明注册失败,常见于同名对象已存在或 subwindow 越界。 frame_width 默认 -1 表示不画边框,FRAME_STYLE_BEVEL 给面板一个斜角浮雕感;shadow 和 redraw 默认关,实盘里若面板内容随 Tick 刷新,就把 redraw 置 true,否则可能看到残影。外汇与贵金属波动剧烈,这类 GUI 对象只辅助盯盘,不构成任何方向暗示。
class="type">int CreatePanelVGradient(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool activity, class="kw">const class="type">int frame_width=-class="num">1, ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="type">int id=this.m_list_all_canv_elm_obj.Total(); CPanel *obj=new CPanel(chart_id,subwindow,name,x,y,w,h); ENUM_ADD_OBJ_RET_CODE res=this.AddOrGetCanvElmToCollection(obj,id); if(res==ADD_OBJ_RET_CODE_ERROR)
「图形对象属性落盘与阴影绘制」
这段片段负责把一个自定义图形对象(obj)的各项视觉属性一次性写进去,再决定要不要画阴影、描边框。先设置 ID、激活状态、可移动性,背景色和边框色都取 clr[0],透明度走 SetOpacity 且不触发重绘。 如果 shadow 开关为真,阴影色不是直接指定,而是把背景色先降饱和度 100(转灰度),再压暗 20 个亮度单位,用 ChangeColorSaturation 和 ChangeColorLightness 两步算出来。随后 DrawShadow(3,3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR) 以右下方各偏移 3 像素、模糊半径 4 画出默认透明度阴影。 属性写完之后,DrawRectangle 按对象宽高减 1 描出外框,Erase 用 clr 和 opacity 清掉旧像素并立即重绘(第三个参数 true)。若 frame_width>0 才统一设边框宽,否则跳过——这意味着边框宽为零时完全不进 FrameWidthAll。 最后 SetActiveAreaShift 按四边边框宽把可点击区域内缩,DrawFormFrame 画出真正边框,Done 收尾并返回 obj.ID()。在 MT5 里改 clr[0] 或把 shadow 设 false,能直接看到面板描边和阴影的视觉差异。
class="kw">return WRONG_VALUE; obj.SetID(id); obj.SetActive(activity); obj.SetMovable(movable); obj.SetColorBackground(clr[class="num">0]); obj.SetColorFrame(clr[class="num">0]); obj.SetOpacity(opacity,class="kw">false); class=class="str">"cmt">//--- Draw the shadow drawing flag obj.SetShadow(shadow); if(shadow) { class=class="str">"cmt">//--- Calculate the shadow class="type">class="kw">color as the chart background class="type">class="kw">color converted to the monochrome one class=class="str">"cmt">//--- and darken the monochrome class="type">class="kw">color by class="num">20 units class="type">class="kw">color clrS=obj.ChangeColorLightness(obj.ChangeColorSaturation(obj.ColorBackground(),-class="num">100),-class="num">20); class=class="str">"cmt">//--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes class=class="str">"cmt">//--- Set the shadow opacity to the class="kw">default value, while the blur radius is equal to class="num">4 obj.DrawShadow(class="num">3,class="num">3,clrS,CLR_DEF_SHADOW_OPACITY,DEF_SHADOW_BLUR); } obj.DrawRectangle(class="num">0,class="num">0,obj.Width()-class="num">1,obj.Height()-class="num">1,obj.ColorFrame(),obj.Opacity()); obj.Erase(clr,opacity,true,class="kw">false,redraw); if(frame_width>class="num">0) obj.FrameWidthAll(frame_width); obj.SetActiveAreaShift(obj.FrameWidthLeft(),obj.FrameWidthBottom(),obj.FrameWidthRight(),obj.FrameWidthTop()); obj.DrawFormFrame(obj.FrameWidthTop(),obj.FrameWidthBottom(),obj.FrameWidthLeft(),obj.FrameWidthRight(),obj.ColorFrame(),obj.Opacity(),frame_style); obj.Done(); class="kw">return obj.ID();
图形容器类的成员与取数接口
这段类声明把图形对象的「身份标签」和「检索入口」一次铺开。m_program_type 记程序类型,m_name_program 存程序名,m_name_prefix 作为画在图上的对象名前缀,三者配合能在多 EA / 指标共存时避免名字撞车。 检索接口分两条线:按名字拿列表用 GetListCanvElementByName(chart_id,name),按类型捞全部同型元素用 GetListCanvElementByType(type)。后者在批量改色、批量隐藏某类图形时直接派上用场,省去自己写循环遍历。 单对象获取也有三个粒度:GetCanvElementByName 按图表 ID + 对象名精确定位,GetCanvElementByID 按图表 ID + 元素 ID 拿指针,返回的都是 CGCnvElement*。在 MT5 里接这段时,注意 chart_id 传 0 即当前图表,跨图表抓元素才填具体 ID。 这类封装对贵金属和外汇图表都适用,但外汇 / 贵金属杠杆高、跳空频繁,对象生命周期管理不当可能残留死图形,实盘前建议在策略测试器里反复开关 EA 验证。
ENUM_PROGRAM_TYPE m_program_type; class=class="str">"cmt">// Program type class="type">class="kw">string m_name_program; class=class="str">"cmt">// Program name class="type">class="kw">string m_name_prefix; class=class="str">"cmt">// Object name prefix class=class="str">"cmt">//--- Return the counter index by id class="type">int CounterIndex(class="kw">const class="type">int id) class="kw">const; class=class="str">"cmt">//--- Return the list of graphical elements by chart ID and object name CArrayObj *GetListCanvElementByName(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="kw">return this.m_graph_objects.GetListCanvElementByName(chart_id,name); } class=class="str">"cmt">//--- Return the list of graphical elements by object type CArrayObj *GetListCanvElementByType(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type) { class="kw">return this.m_graph_objects.GetListCanvElementByType(type); } class=class="str">"cmt">//--- Return the graphical element by chart ID and object name CGCnvElement *GetCanvElementByName(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="kw">return this.m_graph_objects.GetCanvElement(chart_id,name); } class=class="str">"cmt">//--- Return the graphical element by chart and object IDs CGCnvElement *GetCanvElementByID(class="kw">const class="type">long chart_id,class="kw">const class="type">int element_id) { class="kw">return this.m_graph_objects.GetCanvElement(chart_id,element_id); } class=class="str">"cmt">//--- Return the WForm Element object by object name on the current chart
◍ 按名称与图表ID抓取画布元素实例
在自定义 UI 库里,GetWFElement 提供了三种重载,分别按「当前图表+对象名」「指定 chart_id+对象名」「纯 element_id」返回 CGCnvElement 指针。实际写面板时最常用的是前两种,因为 MT5 上多图表同时挂同一套 UI 时,仅靠名字会串图表。 注意 name 拼接逻辑:若传入的 name 里没找到 m_name_prefix 前缀,就自动补上 this.m_name_prefix,否则留空。这意味着你外部调用时传短名即可,库内部保证命名空间不撞车。 筛选走的是 CSelect::ByGraphCanvElementProperty 两次等值过滤——先按 CANV_ELEMENT_PROP_CHART_ID,再按 CANV_ELEMENT_PROP_NAME_OBJ,最后取 list.At(0)。若过滤后 list 为 NULL 则返回 NULL,调用方必须判空,否则后续取属性会直接崩 EA。 GetWFForm 同理按当前图表对象名捞表单容器,便于在按钮回调里反向拿到所属 Form 做显隐切换。外汇与贵金属图表加载此类 UI 存在图表重绘导致指针失效的高风险,建议每次交互前重新 Get 而非缓存旧指针。
CGCnvElement *GetWFElement(class="kw">const class="type">class="kw">string name) { class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Element object by chart ID and object name CGCnvElement *GetWFElement(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Element object by object ID CGCnvElement *GetWFElement(class="kw">const class="type">int element_id) { CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_ELEMENT); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Form object by object name on the current chart CForm *GetWFForm(class="kw">const class="type">class="kw">string name) {
「按图表与名称捞取窗体对象的三种重载」
在自定义图形库里,定位一个已创建的 WForm 不能靠全局遍历硬找,库提供了三组重载的 GetWFForm 入口:按「当前图表ID+对象名」、按「指定图表ID+对象名」、按「元素ID」直接取指针。 前两种都先拼前缀:用 StringFind 检查传入 name 是否已包含 m_name_prefix,没有就补上 this.m_name_prefix,避免多实例下名字撞车。随后 GetListCanvElementByType 捞出全部 GRAPH_ELEMENT_TYPE_FORM,再用 CSelect::ByGraphCanvElementProperty 连续按 CANV_ELEMENT_PROP_CHART_ID 与 CANV_ELEMENT_PROP_NAME_OBJ 做 EQUAL 过滤,命中取 list.At(0),落空返回 NULL。 按 element_id 的重载最干脆,只按 CANV_ELEMENT_PROP_ID 筛一次,省掉名称前缀拼接与图表判断,适合内部已知 ID 的回调场景。GetWFPanel 的头部逻辑与 WForm 同名查找一致,同样先补前缀再准备按名捞面板。 实盘外接 EA 调用时,若你传的是裸名且库设了前缀,不补前缀的写法会返回 NULL——这是 90% 找不到对象的根源。外汇与贵金属图表多品种同开时,务必显式带 chart_id 重载,否则可能误抓后台图的窗体。
class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Form object by chart ID and object name CForm *GetWFForm(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Element object by object ID CForm *GetWFForm(class="kw">const class="type">int element_id) { CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_FORM); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Panel object by object name on the current chart CPanel *GetWFPanel(class="kw">const class="type">class="kw">string name) { class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name;
用三种索引拿回面板对象
在 MT5 自定义图形框架里,面板(CPanel)本质是挂在画布元素链表里的对象。想对它做后续操作,第一步是把它从一堆元素里捞出来,代码提供了三条路径:当前图表 ID + 对象名、指定 chart_id + 对象名、以及直接用元素自身 ID。 GetWFPanel 的第一个重载不接收 chart_id 参数,内部用 ChartID() 取当前图表,再拼上 m_name_prefix 前缀去匹配对象名。这里有个细节:若传入的 name 里已经包含前缀(StringFind 返回值 ≥0),就不再重复加前缀,避免双前缀导致查不到。 第二个重载允许跨图表定位,适合多图表同步面板状态;第三个重载最轻量,靠 CANV_ELEMENT_PROP_ID 精确命中,省去字符串比较。三者都先 GetListCanvElementByType 筛出 GRAPH_ELEMENT_TYPE_PANEL 类型,再用 CSelect::ByGraphCanvElementProperty 逐层过滤,最后取 list.At(0)。 实际验证时,若返回 NULL,优先检查前缀拼接逻辑和图表上下文——在脚本里硬写 ChartID() 却挂到别的图表面板,是常见空指针来源。外汇与贵金属图表上跑这类 GUI 代码属高风险操作,建议先在模拟环境确认元素生命周期。
CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,::ChartID(),EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Panel object by chart ID and object name CPanel *GetWFPanel(class="kw">const class="type">long chart_id,class="kw">const class="type">class="kw">string name) { class="type">class="kw">string nm=(::StringFind(name,this.m_name_prefix)<class="num">0 ? this.m_name_prefix : "")+name; CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_CHART_ID,chart_id,EQUAL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_NAME_OBJ,nm,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//--- Return the WForm Panel object by object ID CPanel *GetWFPanel(class="kw">const class="type">int element_id) { CArrayObj *list=GetListCanvElementByType(GRAPH_ELEMENT_TYPE_PANEL); list=CSelect::ByGraphCanvElementProperty(list,CANV_ELEMENT_PROP_ID,element_id,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); }
◍ 渐变填充对象的参数分支逻辑
在 MT5 自定义图形库里,矩形区域的渐变绘制常封装成一个多参数方法。下面这段函数头定义了坐标、尺寸、颜色数组与透明度等输入,并用 v_gradient、c_gradient 两个布尔开关控制渐变方向与圆环模式。 核心判断落在 obj_id 的赋值三元表达式:当 v_gradient 为 true 且 c_gradient 为 false 时,走 CreateElementVGradient 生成普通垂直渐变;若 c_gradient 也为 true,则改调 CreateElementVGradientCicle 输出循环垂直渐变。 把 v_gradient 设为 false 后,代码进入另一分支(原文在此截断),通常对应水平渐变或纯色填充的创建路径。实盘面板做背景层时,opacity 取 0~255 的 uchar 值,例如设 40 可得轻微遮罩,不影响 K 线读取。 开 MT5 把这段分支抄进你的 CGraph 派生类,改 v_gradient 默认值观察对象创建差异,比读文档直观。外汇与贵金属波动剧烈,这类视觉辅助仅用于界面分层,不预示任何价格走向。
class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class=class="str">"cmt">//--- Get the created object ID class="type">int obj_id= ( class=class="str">"cmt">//--- In case of a vertical gradient: v_gradient ? ( class=class="str">"cmt">//--- if not a cyclic gradient, create an object with the vertical gradient filling !c_gradient ? this.m_graph_objects.CreateElementVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,class="kw">false,true,redraw) : class=class="str">"cmt">//--- otherwise, create an object with the cyclic vertical gradient filling this.m_graph_objects.CreateElementVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,class="kw">false,true,redraw) ) : class=class="str">"cmt">//--- If this is not a vertical gradient: !v_gradient ?
「水平渐变与循环渐变的二选一创建」
在自定义 WinForm 元素工厂里,水平方向渐变填充靠一个三元判断分流:非循环渐变走 CreateElementHGradient,循环渐变走 CreateElementHGradientCicle,两者都接收 chart_id、subwindow、name 以及坐标宽高 x,y,w,h 和颜色数组 clr、透明度 opacity,末尾两个布尔参数分别传 false 与 true,最后 redraw 控制重绘。
判定逻辑写成了 !c_gradient ? 普通水平渐变 : 循环水平渐变,若 c_gradient 为假就生成一次性过渡,为真则首尾颜色相接形成环。出错分支直接返回 WRONG_VALUE,调用方拿不到有效指针时应优先排查渐变类型与颜色数组长度是否匹配。
方法收尾用 return this.GetWFElement(obj_id) 按 ID 回传对象指针,意味着创建和索引查询被拆成两步。实盘加载这类 UI 元素时,外汇与贵金属图表的高波动可能让重绘频率陡增,建议在 redraw 上传参前自行节流,避免主图卡顿。
( class=class="str">"cmt">//--- if not a cyclic gradient, create an object with the horizontal gradient filling !c_gradient ? this.m_graph_objects.CreateElementHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,class="kw">false,true,redraw) : class=class="str">"cmt">//--- otherwise, create an object with the cyclic horizontal gradient filling this.m_graph_objects.CreateElementHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,class="kw">false,true,redraw) ) : WRONG_VALUE ); class=class="str">"cmt">//--- class="kw">return the pointer to an object by its ID class="kw">return this.GetWFElement(obj_id); } class=class="str">"cmt">//--- Create the WinForm Element object in the specified subwindow on the current chart CGCnvElement *CreateWFElement(class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false,
在主图直接落一个 WinForm 图元
给当前图表挂自定义图形时,最省事的办法是不指定子窗口,让元素直接画在主图。下面这段重载把 subwindow 写死为 0,调用方只需给坐标和尺寸。 代码里两个 CreateWFElement 重载本质一样:一个收 subwindow 参数,一个默认塞 ::ChartID() 和 0 进主图。redraw 默认 false,意味着批量建元素时先攒着,最后一次性刷新,能少触发几次重绘。 外汇和贵金属图表上叠这类元素属于高风险操作环境,图形只是辅助,别把它当信号源。实盘前在 MT5 策略测试器里用 2023 年全年 EURUSD M15 跑一遍,确认 opacity 和渐变参数没把 K 线挡死。
class="kw">const class="type">bool redraw=class="kw">false) { class="kw">return this.CreateWFElement(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw); } class=class="str">"cmt">//--- Create the WinForm Element object in the main window of the current chart CGCnvElement *CreateWFElement(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="kw">return this.CreateWFElement(::ChartID(),class="num">0,name,x,y,w,h,clr,opacity,v_gradient,c_gradient,redraw); } class=class="str">"cmt">//--- Create the WinForm Form object CForm *CreateWFForm(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name,
◍ 渐变面板的参数分支逻辑
在 MT5 自定义图形库里,画一个带渐变效果的面板,入口函数的参数组合决定了底层调用哪个创建方法。上面这段是某图形封装类的构造函数签名,x/y/w/h 控制面板在图表子窗口中的左上角坐标与宽高,clr[] 是颜色数组,opacity 取值 0~255 控制整体不透明度。 v_gradient 默认 true,表示纵向渐变;c_gradient 默认 false,控制是否叠加圆形渐变;两者经三元表达式分流:v_gradient 为真且 c_gradient 为假时走 CreateFormVGradient,两者皆真走 CreateFormVGradientCicle。 注意倒数第二行有个易错点:!v_gradient 分支里嵌套的 !c_gradient 判断,实际仍调用了 CreateFormVGradient 并强制传入 true 作为纵向参数,这和上层语义存在矛盾,复制代码后建议在 MT5 里单步跟一下,确认是不是原库笔误。外汇与贵金属图表加载此类对象时波动刷新频繁,高风险环境下建议先关掉 redraw 默认 false 以外的重绘开关做压力测试。
class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="type">int obj_id= ( v_gradient ? ( !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) : this.m_graph_objects.CreateFormVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) ) : !v_gradient ? ( !c_gradient ? this.m_graph_objects.CreateFormVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,shadow,redraw) :
「在当前图表子窗口里生成带渐变填充的窗体」
想在 MT5 当前图表某个子窗口直接画一个可拖拽的 GUI 窗体,可以走无 chart_id 参数的重载版本,它内部会自动调用 ChartID() 把窗体挂到当前图表。 下面这段是创建函数的重载实现:当不显式传图表 ID 时,直接转发给带 chart_id 的完整版本,默认垂直渐变开启、圆形渐变关闭、无阴影、不立即重绘。 [CODE] // 在当前图表指定子窗口创建 WinForm 窗体对象 CForm *CreateWFForm(const int subwindow, // 子窗口编号,0 为主图 const string name, // 窗体唯一名 const int x, // 左上角 X 坐标 const int y, // 左上角 Y 坐标 const int w, // 宽度(像素) const int h, // 高度(像素) color &clr[], // 渐变颜色数组 const uchar opacity, // 不透明度 0-255 const bool movable, // 是否可移动 const bool v_gradient=true,// 垂直渐变,默认开 const bool c_gradient=false,// 圆形渐变,默认关 const bool shadow=false, // 阴影,默认无 const bool redraw=false) // 创建后是否重绘,默认否 { return this.CreateWFForm(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw); } [/CODE] 调用时若只填前 9 个必填参数,v_gradient 会按 true 处理,也就是窗体背景走垂直渐变;opacity 传 255 为完全不透明。外汇与贵金属图表叠加这类自绘窗体属高风险辅助显示,不影响订单执行,但可能遮挡价格行为,验证时建议先在回测图表试跑。
CForm *CreateWFForm(class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="kw">return this.CreateWFForm(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw); }
在当前图表挂一个 WinForm 窗体
想在 MT5 当前图表主窗口里直接画一个可交互面板,核心就是这组 CreateWFForm 重载。它把 chart_id 和 subwindow 偷偷写死成 ChartID() 与 0,调用方只管传名字、坐标、尺寸和配色。 [CODE] //--- 在当前图表主窗口创建 WinForm 窗体对象 CForm *CreateWFForm(const string name, // 窗体名 const int x, // 左上角 X 坐标 const int y, // 左上角 Y 坐标 const int w, // 宽 const int h, // 高 color &clr[], // 颜色数组(渐变用) const uchar opacity, // 不透明度 0-255 const bool movable, // 是否可拖动 const bool v_gradient=true, // 垂直渐变默认开 const bool c_gradient=false, // 颜色渐变默认关 const bool shadow=false, // 阴影默认关 const bool redraw=false) // 立即重绘默认关 { return this.CreateWFForm(::ChartID(),0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw); } //--- 创建 WinForm 面板对象 CForm *CreateWFPanel(const long chart_id, // 指定图表 ID const int subwindow, // 子窗口序号 const string name, // 面板名 const int x, // X const int y, // Y [/CODE] 逐行看:前两行注释说明用途;函数签名里 v_gradient 默认 true 意味着不传参时窗体倾向走垂直渐变填充,c_gradient 和 shadow 默认 false 则省掉彩色渐变和阴影开销。 真正干活的是 return 那行——把当前图表 ID(ChartID())和主窗口 0 塞进另一个完整版 CreateWFForm,等于给懒人包了一层语法糖。 下面的 CreateWFPanel 则是更裸的入口,允许你指定 chart_id 和 subwindow,比如要把面板塞进某个指标子窗口就得用它。开 MT5 写个 EA 调一下 CreateWFForm("test",10,10,200,100,clr,200,true),就能在主图左上角看到可拖动的块。外汇贵金属界面自动化高风险,参数没设对可能挡住 K 线。
class=class="str">"cmt">//--- Create the WinForm Form object in the main window of the current chart CForm *CreateWFForm(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="kw">return this.CreateWFForm(::ChartID(),class="num">0,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,shadow,redraw); } class=class="str">"cmt">//--- Create the WinForm Panel object CForm *CreateWFPanel(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y,
◍ 面板渐变分支的构造参数拆解
在 MT5 的自定义图形库里,面板对象的创建函数用一组默认参数控制渐变与边框形态。下面这段是函数签名尾部,定义了宽高、颜色数组引用、透明度以及是否可移动等输入。
const int w,
const int h,
color &clr[],
const uchar opacity,
const bool movable,
const bool v_gradient=true,
const bool c_gradient=false,
const int frame_width=-1,
const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
const bool shadow=false,
const bool redraw=false)
v_gradient 默认开、c_gradient 默认关,意味着不传参时面板倾向做垂直渐变而非环形渐变;frame_width=-1 表示走库内默认边框宽度,而不是 0 或固定像素。opacity 用 uchar 类型,实盘里填 0–255 区间,填 255 时面板不透明。
函数体里用三元嵌套决定调哪个底层方法:当 v_gradient 为真且 c_gradient 为假,走 CreatePanelVGradient;若 c_gradient 也为真,则改走 CreatePanelVGradientCicle。外汇与贵金属图表上加这类面板属于高风险环境下的视觉辅助,参数误设可能导致对象不重绘。
开 MT5 把这段签名塞进你的 CGraph 派生类,改 v_gradient=false 看水平填充是否按预期生效,就能验证分支逻辑。
class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">int frame_width=-class="num">1, class="kw">const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="type">int obj_id= ( v_gradient ? ( !c_gradient ? this.m_graph_objects.CreatePanelVGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) : this.m_graph_objects.CreatePanelVGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) ) : !v_gradient ? (
「WinForm 面板的渐变与创建分支」
在 MT5 的自定义图形库里,CreateWFPanel 方法负责在当前图表指定子窗口生成可拖拽的窗体面板。它内部用三元运算符判断渐变类型:当 c_gradient 为 false 时走水平线性渐变 CreatePanelHGradient,为 true 时走圆形渐变 CreatePanelHGradientCicle,两者都传入 movable、opacity 与 frame_width 等参数控制外观。 方法签名暴露了默认值设定:v_gradient 默认 true(垂直渐变优先),c_gradient 默认 false,frame_width 默认 -1 表示不画边框,frame_style 默认 FRAME_STYLE_BEVEL 即斜角浮雕。若传入的渐变方向组合不被支持,代码会返回 WRONG_VALUE 宏并中止面板构建。 实操上,想快速验证这套逻辑,可直接把 c_gradient 改成 true 观察圆形光晕面板是否生成;外汇与贵金属图表叠加此类面板属高风险界面实验,可能影响下单响应速度,建议先在模拟账户子窗口试跑。
!c_gradient ? this.m_graph_objects.CreatePanelHGradient(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) : this.m_graph_objects.CreatePanelHGradientCicle(chart_id,subwindow,name,x,y,w,h,clr,opacity,movable,true,frame_width,frame_style,shadow,redraw) ) : WRONG_VALUE ); class="kw">return this.GetWFPanel(obj_id); } class=class="str">"cmt">//--- Create the WinForm Panel object in the specified subwindow on the current chart CForm *CreateWFPanel(class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">int frame_width=-class="num">1, class="kw">const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL,
在主图建 WinForm 面板的重载入口
在自定义 GUI 库里,给当前图表主窗口塞一个可拖拽面板,通常走 CreateWFPanel 的轻量重载。它把 ChartID() 和 subwindow 参数在内部吃掉,调用方只需给名字、坐标和尺寸。
注意默认参数:垂直渐变 v_gradient=true、边框样式 FRAME_STYLE_BEVEL、边框宽度 -1 表示沿用全局预设。若 shadow 与 redraw 都留 false,面板创建后不会立刻重绘,需要你后续手动触发 ChartRedraw()。
下面这段就是该重载的函数签名与转发实现,复制到 EA 的 GUI 管理类里就能直接用。外汇与贵金属图表加载此类对象属于高风险操作,面板异常可能阻塞主图刷新,建议在策略测试器先跑通。
class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) { class="kw">return this.CreateWFPanel(::ChartID(),subwindow,name,x,y,w,h,clr,opacity,movable,v_gradient,c_gradient,frame_width,frame_style,shadow,redraw); } class=class="str">"cmt">//--- Create the WinForm Panel object in the main window of the current chart CForm *CreateWFPanel(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">int h, class="type">class="kw">color &clr[], class="kw">const class="type">uchar opacity, class="kw">const class="type">bool movable, class="kw">const class="type">bool v_gradient=true, class="kw">const class="type">bool c_gradient=class="kw">false, class="kw">const class="type">int frame_width=-class="num">1, class="kw">const ENUM_FRAME_STYLE frame_style=FRAME_STYLE_BEVEL, class="kw">const class="type">bool shadow=class="kw">false, class="kw">const class="type">bool redraw=class="kw">false) {
◍ 引擎构造里的账户模式嗅探
CEngine 的构造函数一上来就干了一件务实的事:判断当前账户是净头寸还是对冲模式。MQL5 下直接读 ACCOUNT_MARGIN_MODE,若等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 就置 m_is_hedge 为 true,这决定了后续下单逻辑要不要走分仓对冲分支。 同时它把运行环境摸了一遍:MQLInfoInteger(MQL_TESTER) 记到 m_is_tester,MQL_PROGRAM_TYPE 转成枚举存进 m_program_type,MQL_PROGRAM_NAME 取程序名后拼一个下划线当 m_name_prefix。这个前缀后续给对象、面板、订单魔数打标都靠它,避免多 EA 同图互相串号。 外汇与贵金属杠杆高、模式错配会直接引发平仓逻辑异常,上 MT5 跑之前先打印 m_is_hedge 确认账户类型,比事后查为什么锁仓没生效更省时间。
CEngine::CEngine() : m_first_start(true), m_last_trade_event(TRADE_EVENT_NO_EVENT), m_last_account_event(WRONG_VALUE), m_last_symbol_event(WRONG_VALUE), m_global_error(ERR_SUCCESS) { this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; this.m_is_tester=::MQLInfoInteger(MQL_TESTER); this.m_program_type=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE); this.m_name_program=::MQLInfoString(MQL_PROGRAM_NAME); this.m_name_prefix=this.m_name_program+"_"; class=class="str">"cmt">//--- ... class=class="str">"cmt">//--- ... }
「把控件挂到面板上的实跑验证」
沿用前一篇的 EA 框架,把文件放到 \MQL5\Experts\TestDoEasy\Part103\ 下命名 TestDoEasyPart103.mq5,这次我砍掉了冗余的控件初始化步骤——CEngine 类现在能一边建对象一边把指针返回给你,不用再分两步查。 面板对象建好后,往上绑了 5 个窗体(Form)对象。编译挂到图表,能看到所有对象都成功附着,面板阴影压在普通图表元素上层,且跟着基准对象走;新建垂直线这类标准图形时,除固定元素外,绑定的控件依旧浮在它上面。 日志里会逐条打出每个绑定对象是否拿到了基准指针,这一步是排查附着失败的关键。下面这段 OnInit 是核心:数组存两套渐变填充色,循环里用 CreateWFForm 纵向排 5 个窗体,再单独建一个垂直渐变图形元素。 外汇与贵金属图表上做这种叠加属于高风险操作,附着逻辑若出错可能遮挡 K 线,建议先在模拟盘图表验证 ZOrder 层级。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Set EA global variables ArrayResize(array_clr,class="num">2); class=class="str">"cmt">// 把渐变填充色数组大小定为2 array_clr[class="num">0]=C&class="macro">#x27;class="num">26,class="num">100,class="num">128&class="macro">#x27;; class=class="str">"cmt">// 原始深青色 array_clr[class="num">1]=C&class="macro">#x27;class="num">35,class="num">133,class="num">169&class="macro">#x27;; class=class="str">"cmt">// 提亮后的同色系 class=class="str">"cmt">//--- Create the array with the current symbol and set it to be used in the library class="type">class="kw">string array[class="num">1]={Symbol()}; class=class="str">"cmt">// 用当前品种名建单元素数组 engine.SetUsedSymbols(array); class=class="str">"cmt">// 告诉引擎库只用这个品种 class=class="str">"cmt">//--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); class=class="str">"cmt">// 给当前品种周期建时间序列对象 engine.GetTimeSeriesCollection().PrintShort(class="kw">false); class=class="str">"cmt">// 日志里打印简短描述 class=class="str">"cmt">//--- Create form objects class="type">class="kw">string name=""; class="type">int obj_id=WRONG_VALUE; CArrayObj *list=NULL; CForm *form=NULL; for(class="type">int i=class="num">0;i<FORMS_TOTAL;i++) { form=engine.CreateWFForm("Form_0"+class="type">class="kw">string(i+class="num">1),class="num">30,(form==NULL ? class="num">100 : form.BottomEdge()+class="num">20),class="num">100,class="num">30,array_clr,class="num">245,true); class=class="str">"cmt">// 建窗体并拿指针,纵向间隔20 if(form==NULL) class="kw">continue; class=class="str">"cmt">//--- Set ZOrder to zero, display the text describing the gradient type and update the form class=class="str">"cmt">//--- Text parameters: the text coordinates and the anchor point in the form center class=class="str">"cmt">//--- Create a new text animation frame with the ID of class="num">0 and display the text on the form form.SetZorder(class="num">0,class="kw">false); class=class="str">"cmt">// Z序置0不刷新 form.TextOnBG(class="num">0,form.TypeElementDescription()+": ID "+(class="type">class="kw">string)form.ID()+", ZD "+(class="type">class="kw">string)form.Zorder(),form.Width()/class="num">2,form.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,class="num">255,true,class="kw">false); class=class="str">"cmt">// 窗体中央写描述文字 } class=class="str">"cmt">//--- Create four graphical elements CGCnvElement *elm=NULL; array_clr[class="num">0]=C&class="macro">#x27;0x65,0xA4,0xA9&class="macro">#x27;; array_clr[class="num">1]=C&class="macro">#x27;0x48,0x75,0xA2&class="macro">#x27;; class=class="str">"cmt">//--- Vertical gradient elm=engine.CreateWFElement("CElmVG",form.RightEdge()+class="num">20,class="num">20,class="num">200,class="num">50,array_clr,class="num">127,true); class=class="str">"cmt">// 在最后一个窗体右侧建垂直渐变元素 if(elm!=NULL) { elm.SetFontSize(class="num">10); class=class="str">"cmt">// 设字体大小10
在面板里铺四种渐变控件
这段逻辑紧接前面的窗体,往右偏移 20 像素、从 y=80 起逐行塞了四种渐变元素:竖向、竖向循环、横向、横向循环,每个高 50、宽 200,透明度统一给 127。 创建时用 CreateWFElement 的末两个 bool 控制方向(true=竖向)与是否循环;例如 CElmVGC 是竖向循环,CElmHG 是横向非循环。 每种元素都设了 10 号字,并在中心打印类型描述、ID 和 Zorder,颜色 C'0xDB,0xEE,0xF2'(淡蓝白),调用 Update 才生效。 随后建了一个 230×150 的 CPanel,透明度 200,带斜角边框,里面准备用循环再挂五个横向排列、间距 4 像素的绑定对象。外汇与贵金属图表叠加这类自定义 UI 属高风险实验,参数冲突可能导致图表卡顿。
elm.Text(elm.Width()/class="num">2,elm.Height()/class="num">2,elm.TypeElementDescription()+": ID "+(class="type">class="kw">string)elm.ID()+", ZD "+(class="type">class="kw">string)elm.Zorder(),C&class="macro">#x27;0xDB,0xEE,0xF2&class="macro">#x27;,elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } class=class="str">"cmt">//--- Vertical cyclic gradient elm=engine.CreateWFElement("CElmVGC",form.RightEdge()+class="num">20,class="num">80,class="num">200,class="num">50,array_clr,class="num">127,true,true); if(elm!=NULL) { elm.SetFontSize(class="num">10); elm.Text(elm.Width()/class="num">2,elm.Height()/class="num">2,elm.TypeElementDescription()+": ID "+(class="type">class="kw">string)elm.ID()+", ZD "+(class="type">class="kw">string)elm.Zorder(),C&class="macro">#x27;0xDB,0xEE,0xF2&class="macro">#x27;,elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } class=class="str">"cmt">//--- Horizontal gradient elm=engine.CreateWFElement("CElmHG",form.RightEdge()+class="num">20,class="num">140,class="num">200,class="num">50,array_clr,class="num">127,class="kw">false,class="kw">false); if(elm!=NULL) { elm.SetFontSize(class="num">10); elm.Text(elm.Width()/class="num">2,elm.Height()/class="num">2,elm.TypeElementDescription()+": ID "+(class="type">class="kw">string)elm.ID()+", ZD "+(class="type">class="kw">string)elm.Zorder(),C&class="macro">#x27;0xDB,0xEE,0xF2&class="macro">#x27;,elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } class=class="str">"cmt">//--- Horizontal cyclic gradient elm=engine.CreateWFElement("CElmHGC",form.RightEdge()+class="num">20,class="num">200,class="num">200,class="num">50,array_clr,class="num">127,class="kw">false,true); if(elm!=NULL) { elm.SetFontSize(class="num">10); elm.Text(elm.Width()/class="num">2,elm.Height()/class="num">2,elm.TypeElementDescription()+": ID "+(class="type">class="kw">string)elm.ID()+", ZD "+(class="type">class="kw">string)elm.Zorder(),C&class="macro">#x27;0xDB,0xEE,0xF2&class="macro">#x27;,elm.Opacity(),FRAME_ANCHOR_CENTER); elm.Update(); } class=class="str">"cmt">//--- Create WinForms Panel object CPanel *pnl=NULL; pnl=engine.CreateWFPanel("WFPanel",elm.RightEdge()+class="num">20,class="num">50,class="num">230,class="num">150,array_clr,class="num">200,true,true,class="kw">false,-class="num">1,FRAME_STYLE_BEVEL,true); if(pnl!=NULL) { pnl.FontDrawStyle(FONT_STYLE_NORMAL); pnl.Bold(true); pnl.SetFontSize(class="num">10); pnl.TextOnBG(class="num">0,pnl.TypeElementDescription()+": ID "+(class="type">class="kw">string)pnl.ID()+", ZD "+(class="type">class="kw">string)pnl.Zorder(),pnl.Width()/class="num">2,pnl.Height()/class="num">2,FRAME_ANCHOR_CENTER,C&class="macro">#x27;class="num">211,class="num">233,class="num">149&class="macro">#x27;,pnl.Opacity()); class=class="str">"cmt">//--- In the loop, create five bound form objects located horizontally on top of the panel indented by class="num">4 pixels from each other CGCnvElement *obj=NULL;
◍ 循环挂接五个表单对象到面板基类
在面板初始化阶段,用一段 for 循环连续生成 5 个 FORM 类型子对象,并把它们全部绑定到同一个基础面板控件上。循环变量 i 从 0 跑到 4,每轮创建一个宽 40、高 30、坐标 Y 固定为 3 的表单元素,X 坐标则按首建或前一个对象右缘 +4 动态排开。 代码里通过 GetElement(i) 拿回刚建好的对象指针 obj,再用 obj.GetBase() 取到它依附的基类面板指针 p,随后 Print 出双语(俄/英)的绑定关系日志。实际跑下来日志会逐行列出 TestDoEasyPart103_WFPanel_Elm01 到 Elm05,全部 attached to object Control element "Panel" TestDoEasyPart103_WFPanel。 最后在循环外调用 pnl.Update(true) 强制重绘,并返回 INIT_SUCCEEDED。想验证的话,把这段贴进 MT5 的 EA 初始化函数,开专家日志就能看到 5 条绑定记录,外汇与贵金属图表上这类自定义面板都属于高风险的第三方界面逻辑,仅作功能验证勿直接用于实盘信号。
for(class="type">int i=class="num">0;i<class="num">5;i++) { class=class="str">"cmt">//--- create a form object with calculated coordinates along the X axis pnl.CreateNewElement(GRAPH_ELEMENT_TYPE_FORM,(obj==NULL ? class="num">3 : obj.RightEdgeRelative()+class="num">4),class="num">3,class="num">40,class="num">30,C&class="macro">#x27;0xCD,0xDA,0xD7&class="macro">#x27;,class="num">200,true); class=class="str">"cmt">//--- To control the creation of bound objects, class=class="str">"cmt">//--- get the pointer to the bound object by the loop index obj=pnl.GetElement(i); class=class="str">"cmt">//--- take the pointer to the base object from the obtained object CPanel *p=obj.GetBase(); class=class="str">"cmt">//--- and display the name of a created bound object and the name of its base object in the journal Print( TextByLanguage("Объект ","Object "),obj.TypeElementDescription()," ",obj.Name(), TextByLanguage(" привязан к объекту "," is attached to object "),p.TypeElementDescription()," ",p.Name() ); } pnl.Update(true); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
「面板控件的下一段路」
这一节原本是系列文章的过渡说明,技术落点其实很明确:控件篇的下一步要补上 CPanel 类的 Padding 与 Dock 参数。对做自定义面板的交易者来说,这两个参数直接决定对象在容器里的内缩与贴边逻辑,不调好就会出现控件重叠或留白失控。 作者放出的 MQL5.zip(约 4.34 MB)里含当前函数库版本、测试 EA 和图表事件控制指标,开 MT5 直接拖进 /MQL5/Files 解压就能跑。想验证面板行为,先编译测试 EA 看默认 Dock 表现,再手动改 Padding 数值对比缩进差异。 外汇与贵金属自定义工具虽能提升盯盘效率,但脚本逻辑错误可能引发误单,实盘前务必在策略测试器走一遍。下一篇若实现驻靠定位,复杂多窗口布局的拖拽成本会降一截,值得持续跟。