DoEasy. 控件 (第 21 部分): SplitContainer 控件 面板隔板·综合运用
(3/3)· 54 节收官,把 MS 风格隔板拖拽逻辑塞进 MQL5 基类,省掉重复造轮子的功夫
- 工作流控件类型的字符串映射
- 拆开分栏容器的结构体字段
- 图表滚动区与边框区的坐标变量定义
- 控件基类的光标命中与资源存档
- 画布控件的可交互区域怎么算
- 画布控件的滚动区与边缘热区取值
- 图形元素构造时的参数与初始化落点
- 画布控件初始化时的属性落点
- 拆分容器与画布元素的初始化参数
- 图形元素基类的受保护构造逻辑
- 画布控件的坐标与边界初始化
- 拆开分栏容器的界面属性初始化
- 从画布对象抓取分隔容器与控制区坐标
- 把画布控件几何与文本塞进结构体
- 结构体到界面对象的属性灌入
- 画布控件的滚动区与命中判定落地
- 表单显示与鼠标区域位标记法
- 从位掩码到表单鼠标状态机的落地
- 表单内不同鼠标区域的分流处理
- 鼠标空闲态下的界面复位逻辑
- 鼠标状态分支与重绘逻辑的落点
- 控件属性描述与重绘触发的底层逻辑
- 控件区域与滚动区的属性回显分支
- 画布控件属性枚举的尾段拼接逻辑
- 拆开分栏容器的最小尺寸属性读取
- 隔板对象类怎么撑起可拖拽分隔
- 分隔条控件的类骨架与构造约束
- 分隔控件的两种构造入口
- 分隔控件的隐藏与重绘逻辑
- 棋盘格像素与分栏容器骨架
- 面板与分隔条的坐标成员及对象创建接口
- 拆分容器里抓取面板与分隔条的指针
- 拆开分栏容器的折叠与分隔属性
- 拆分容器里固定面板的读写与子元素挂载
- 拆分容器里怎么造图形元素
- 拆分式容器的面板与分隔条装配
- 双面板布局的折叠坐标算法
- 横向分隔下的面板坐标推算
- 折叠态下双面板的坐标重算逻辑
- 面板互斥折叠的逻辑实现
- 分隔条的距离与宽度怎么落进控件区域
- 分隔条拖拽时的坐标边界约束
- 拖拽分隔条时双面板坐标重算
- 拆分容器面板的对象类骨架
- 折叠面板的标志位与事件回调
- 拆分容器里新图形对象的生成入口
- 图形元素工厂的 case 分发逻辑
- 标签与箭头类控件的实例化分支
- 折叠态与悬停分离器的显隐逻辑
- WinForms 控件的配色分支怎么落
- 分隔条与箭头按钮的样式分支处理
- 分隔条拖动后的坐标收敛逻辑
- SplitContainer 文本标签的坐标修正与偶发冻结
- 画得少,看得清
◍ 工作流控件类型的字符串映射
在 MT5 的自定义图形框架里,工作流(WF)系列控件靠一个枚举 GRAPH_ELEMENT_TYPE_WF_* 区分身份。下面这段三元表达式链把枚举值逐个翻译成可读文本,调试面板或日志里看类型不再是一串数字。
注意高亮的 GRAPH_ELEMENT_TYPE_WF_SPLITTER 分支——它对应可拖拽分割条,很多自制面板布局错乱,根源就是漏判了这个类型,全部跌进末尾的 "Unknown" 兜底分支。
私有段里 m_shift_coord_x / m_shift_coord_y 两个 int 偏移量,是控件相对基准对象的像素位移;改这两个值,不用动控件本身就能整体平移子元素。外汇与贵金属 GUI 自动化涉及高频重绘,这类偏移计算失误可能引发闪崩式卡顿,属高风险操作。
type==GRAPH_ELEMENT_TYPE_WF_CHECKBOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKBOX) : type==GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON) : type==GRAPH_ELEMENT_TYPE_WF_BUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON) : type==GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ELEMENTS_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM) : type==GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX) : type==GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX) : class=class="str">"cmt">//--- Auxiliary control objects type==GRAPH_ELEMENT_TYPE_WF_TAB_HEADER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_HEADER) : type==GRAPH_ELEMENT_TYPE_WF_TAB_FIELD ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_TAB_FIELD) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT) : type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX): type==GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX): type==GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL) : type==GRAPH_ELEMENT_TYPE_WF_SPLITTER ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_SPLITTER) : "Unknown" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private: class="type">int m_shift_coord_x; class=class="str">"cmt">// Offset of the X coordinate relative to the base object class="type">int m_shift_coord_y; class=class="str">"cmt">// Offset of the Y coordinate relative to the base object class="kw">struct SData { class=class="str">"cmt">//--- Object integer properties
拆开分栏容器的结构体字段
在 MT5 自定义图形界面里,分栏容器(split container)靠一组结构体字段控制布局,下面这段声明直接决定了面板怎么分、分隔条怎么动。 id 是元素唯一编号,type 标记图形元素类别,displayed 控制该控件是否参与隐藏逻辑(false 时可能被后台管理但不绘制)。 split_container_fixed_panel 指定缩放容器时保持尺寸的面板编号;split_container_splitter_fixed 为 false 时分隔条可被拖拽,true 则锁死。split_container_splitter_distance 是边缘到分隔条的像素距离,split_container_splitter_width 定义拖拽条自身宽度(常见取值 4~8),split_container_splitter_orientation 控制横向或纵向分割。 两个面板各有折叠标志与最小尺寸:panel1/panel2_collapsed 为 true 时对应面板收起,_min_size 防止缩放时压到不可用的厚度。 control_area_x/y/width/height 框出控件可交互区域的坐标与大小,scroll_area_x_right 则是右侧滚动区的 X 基准——改这几个值,面板在图表上的占位立刻变。外汇与贵金属图表挂 EA 面板属高风险操作,参数误写可能导致界面溢出或事件错位。 让小布替你跑这套 把 split_container_splitter_distance 从默认 100 改成 ChartGetInteger(0,CHART_WIDTH_IN_PIXELS)/2,分隔条会直接跳到屏幕中线,比手动拖更准。
class="type">int id; class=class="str">"cmt">// Element ID class="type">int type; class=class="str">"cmt">// Graphical element type class=class="str">"cmt">//---... class=class="str">"cmt">//---... class="type">bool displayed; class=class="str">"cmt">// Non-hidden control display flag class="type">int split_container_fixed_panel; class=class="str">"cmt">// Panel that retains its size when the container is resized class="type">bool split_container_splitter_fixed; class=class="str">"cmt">// Separator moveability flag class="type">int split_container_splitter_distance; class=class="str">"cmt">// Distance from edge to separator class="type">int split_container_splitter_width; class=class="str">"cmt">// Separator width class="type">int split_container_splitter_orientation; class=class="str">"cmt">// Separator location class="type">bool split_container_panel1_collapsed; class=class="str">"cmt">// Flag for collapsed panel class="num">1 class="type">int split_container_panel1_min_size; class=class="str">"cmt">// Panel class="num">1 minimum size class="type">bool split_container_panel2_collapsed; class=class="str">"cmt">// Flag for collapsed panel class="num">2 class="type">int split_container_panel2_min_size; class=class="str">"cmt">// Panel class="num">2 minimum size class="type">int control_area_x; class=class="str">"cmt">// Control area X coordinate class="type">int control_area_y; class=class="str">"cmt">// Control area Y coordinate class="type">int control_area_width; class=class="str">"cmt">// Control area width class="type">int control_area_height; class=class="str">"cmt">// Control area height class="type">int scroll_area_x_right; class=class="str">"cmt">// Right scroll area X coordinate
「图表滚动区与边框区的坐标变量定义」
在 MT5 自定义图形界面里,先把右侧、底部滚动区和四周边框的命中范围用整型变量锁死,后面做鼠标交互才不会算错点击区域。 下面这段结构成员直接给出了右滚动区 Y 坐标、宽高,底滚动区 X/Y 坐标、宽高,以及左、底、右、上四条边缘区的宽度,全部以像素为单位在客户端坐标系内生效。 对象自身的名称、资源名和显示文本则用定长 uchar 数组承接:name_obj 与 name_res 各占 64 字节,text 占 256 字节,足以覆盖 MT5 图形资源命名与标签文案的常见长度。 开 MT5 在结构体里补上这些字段后,用 ChartXYToTimePrice 反向校验坐标,能直接看出边缘区宽度设成多少时拖拽不误触。
class="type">int scroll_area_y_right; class=class="str">"cmt">// Right scroll area Y coordinate class="type">int scroll_area_width_right; class=class="str">"cmt">// Right scroll area width class="type">int scroll_area_height_right; class=class="str">"cmt">// Right scroll area height class="type">int scroll_area_x_bottom; class=class="str">"cmt">// Bottom scroll area X coordinate class="type">int scroll_area_y_bottom; class=class="str">"cmt">// Bottom scroll area Y coordinate class="type">int scroll_area_width_bottom; class=class="str">"cmt">// Bottom scroll area width class="type">int scroll_area_height_bottom; class=class="str">"cmt">// Bottom scroll area height class="type">int border_left_area_width; class=class="str">"cmt">// Left edge area width class="type">int border_bottom_area_width; class=class="str">"cmt">// Bottom edge area width class="type">int border_right_area_width; class=class="str">"cmt">// Right edge area width class="type">int border_top_area_width; class=class="str">"cmt">// Upper edge area width class=class="str">"cmt">//--- Object real properties class=class="str">"cmt">//--- Object class="type">class="kw">string properties class="type">uchar name_obj[class="num">64]; class=class="str">"cmt">// Graphical element object name class="type">uchar name_res[class="num">64]; class=class="str">"cmt">// Graphical resource name class="type">uchar text[class="num">256]; class=class="str">"cmt">// Graphical element text
◍ 控件基类的光标命中与资源存档
做 MT5 自定义图形面板时,最容易被忽略的是「光标到底落在哪一层」这件事。下面这段基类声明把元素拆成整块、激活区、控制区三层,分别提供 CursorInsideElement / CursorInsideActiveArea / CursorInsideControlArea,传入 x、y 即可拿到布尔命中结果,避免按钮热区被背景画布吞掉。 资源处理上,ResourceStamp(const string source) 负责把图形资源塞进 256 字节的 descript 数组成像并回写,Reset() 做虚函数级清理,两者配合能在图表重载时恢复控件外观,不用每次重绘都从硬盘读文件。 Create() 的参数表是实打实的落地细节:chart_id、wnd_num 定位图表子窗口,x/y/w/h 定像素盒,redraw 默认 false。若你做的黄金 EA 面板需要在 tick 外被动重绘,记得显式传 true,否则控件可能停在上一帧。 外汇与贵金属图表挂自定义控件属于高风险改造,坐标算错会导致点击误触发下单,建议在策略测试器里先用模拟图表验证命中逻辑再上真实账户。
class="type">uchar descript[class="num">256]; class=class="str">"cmt">// Graphical element description }; SData m_struct_obj; class=class="str">"cmt">// Object structure class=class="str">"cmt">//--- (class="num">1) Save the graphical resource to the array and(class="num">2) restore the resource from the array class="type">bool ResourceStamp(class="kw">const class="type">class="kw">string source); class="kw">virtual class="type">bool Reset(class="type">void); class=class="str">"cmt">//--- Return the cursor position relative to the(class="num">1) entire element, (class="num">2) the element active area and(class="num">3) control area class="type">bool CursorInsideElement(class="kw">const class="type">int x,class="kw">const class="type">int y); class="type">bool CursorInsideActiveArea(class="kw">const class="type">int x,class="kw">const class="type">int y); class="type">bool CursorInsideControlArea(class="kw">const class="type">int x,class="kw">const class="type">int y); class=class="str">"cmt">//--- Create the element class="type">bool Create(class="kw">const class="type">long chart_id, class="kw">const class="type">int wnd_num, 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">bool redraw=class="kw">false); class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the flag for displaying a non-hidden control class="type">void SetDisplayed(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,flag); } class="type">bool Displayed(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_DISPLAYED); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the graphical element type class=class="str">"cmt">//--- Return the coordinate(class="num">1) of the left, (class="num">2) right, (class="num">3) top and(class="num">4) bottom edge of the element active area
画布控件的可交互区域怎么算
在 MT5 自定义指标或面板里,一个图形元素往往不止有可见边界,还有「可点击 / 可拖拽」的活跃区与控制区。下面这组方法直接给出以像素为单位的四至坐标,省去你手动加减边距。 ActiveAreaLeft 返回元素左边界 CoordX() 加上左偏移 ActiveAreaLeftShift(),转成 int;ActiveAreaRight 则是右边界 RightEdge() 减去右偏移。上下同理,Top 用 CoordY() 加偏移,Bottom 用 BottomEdge() 减偏移。 控制区(ControlArea)不走偏移计算,而是从对象属性里取:CANV_ELEMENT_PROP_CONTROL_AREA_X / Y / WIDTH / HEIGHT 四个枚举分别对应 ControlAreaX / Y / Width / Height。右侧滚动区则用 CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT 与 _Y_RIGHT 取 ScrollAreaXRight / YRight。 开 MT5 新建一个 Canvas 面板,把这几行挂到某按钮类里,Print 出 ControlAreaWidth 的值,就能确认你的热点区域是否比可见图小了 2~3 像素——外汇与贵金属图表交互在高波动时易点偏,这种偏差值得当面验证。
class="type">int ActiveAreaLeft(class="type">void) class="kw">const { class="kw">return class="type">int(this.CoordX()+this.ActiveAreaLeftShift()); } class="type">int ActiveAreaRight(class="type">void) class="kw">const { class="kw">return class="type">int(this.RightEdge()-this.ActiveAreaRightShift()); } class="type">int ActiveAreaTop(class="type">void) class="kw">const { class="kw">return class="type">int(this.CoordY()+this.ActiveAreaTopShift()); } class="type">int ActiveAreaBottom(class="type">void) class="kw">const { class="kw">return class="type">int(this.BottomEdge()-this.ActiveAreaBottomShift()); } class=class="str">"cmt">//--- Return the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) width and(class="num">4) height of the element control area height class="type">int ControlAreaX(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X); } class="type">int ControlAreaY(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y); } class="type">int ControlAreaWidth(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH); } class="type">int ControlAreaHeight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT); } class=class="str">"cmt">//--- Return the(class="num">1) X, (class="num">2) Y coordinates, (class="num">3) width and(class="num">4) height of the element right scroll area height class="type">int ScrollAreaXRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT); } class="type">int ScrollAreaYRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT); }
「画布控件的滚动区与边缘热区取值」
在 MT5 的自定义图形库里,画布元素把滚动触发区和拖拽缩放区拆成了独立属性,靠一组 getter 直接读回像素值。右侧和下方的滚动区分别用 ScrollArea*Right 与 ScrollArea*Bottom 系列返回 X/Y/宽/高,数值类型被强转成 int,意味着亚像素精度在这里被丢弃。 边缘缩放热区则用 BorderResizeAreaLeft/Right/Top/Bottom 四个方法取回对应边的可拖拽宽度,返回值同样是 int 像素。若你在写可拖拽面板,这些值直接决定了鼠标落到哪几个像素内才会变成缩放光标。 下面这段是原文里的接口声明,逐行看一遍就能明白每个方法只是对 GetProperty 的薄封装: int ScrollAreaWidthRight(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT); } // 返回右侧滚动区宽度,从元素属性里读 CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT 并转 int int ScrollAreaHeightRight(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT); } // 返回右侧滚动区高度 int ScrollAreaXBottom(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM); } // 返回底部滚动区 X 坐标 int ScrollAreaYBottom(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM); } // 返回底部滚动区 Y 坐标 int ScrollAreaWidthBottom(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM); } // 返回底部滚动区宽度 int ScrollAreaHeightBottom(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM); } // 返回底部滚动区高度 int BorderResizeAreaLeft(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH); } // 返回左边框可拖拽缩放区宽度 int BorderResizeAreaRight(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH); } // 返回右边框可拖拽缩放区宽度 int BorderResizeAreaTop(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH); } // 返回上边框可拖拽缩放区宽度 int BorderResizeAreaBottom(void) const { return (int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH); } // 返回下边框可拖拽缩放区宽度 实盘里做自定义 HUD 面板时,外汇与贵金属波动快、高风险,建议把 BorderResizeArea* 设成 4~6 像素,太窄在 4K 屏上几乎点不中,太宽会和内部滚动区抢事件。开 MT5 新建个 Canvas 面板,打印这几个属性就能验证默认是多少。
class="type">int ScrollAreaWidthRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT); } class="type">int ScrollAreaHeightRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT); } class="type">int ScrollAreaXBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM); } class="type">int ScrollAreaYBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM); } class="type">int ScrollAreaWidthBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM); } class="type">int ScrollAreaHeightBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM); } class="type">int BorderResizeAreaLeft(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH); } class="type">int BorderResizeAreaRight(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH); } class="type">int BorderResizeAreaTop(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH); } class="type">int BorderResizeAreaBottom(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH); }
◍ 图形元素构造时的参数与初始化落点
在 MT5 自定义图形元素类里,构造函数一口气吃进十几个参数:元素编号、图表 ID、子窗口号、描述文本、坐标 x/y、宽高 w/h、颜色、透明度,以及 movable / activity / redraw 三个布尔开关,默认分别是 true、true、false。 下面这段是构造体头部与初始化核心,直接决定元素挂到哪张图、哪个子窗口、背景色怎么取。 代码里先调 ChartGetInteger 拿图表背景色存进 m_chart_color_bg,若 chart_id 为空就退回 ChartID() 当前图;m_name 由 CreateNameGraphElement 按类型生成,m_chart_id 同理兜底。随后 SetFont 用默认字体与字号,文本锚点和偏移量全清 0,再 SetBackgroundColor 并 SetOpacity 写入透明度。 最后两个 ArrayResize 把背景色数组和按下态背景色数组各扩到 1 格,成功后才把当前背景色写进 m_array_colors_bg[0]。开 MT5 把这段粘进你的元素类,改 DEF_FONT_SIZE 就能看到标签字号变化。外汇与贵金属图表叠加自绘元素时杠杆风险高,参数误设可能导致图形错位。
class="kw">const class="type">int element_num, class="kw">const class="type">long chart_id, class="kw">const class="type">int wnd_num, class="kw">const class="type">class="kw">string descript, 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=true, class="kw">const class="type">bool activity=true, class="kw">const class="type">bool redraw=class="kw">false) : m_shadow(class="kw">false) { this.SetTypeElement(element_type); this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_chart_color_bg=(class="type">class="kw">color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND); this.m_name=this.CreateNameGraphElement(element_type); this.m_chart_id=(chart_id==NULL || chart_id==class="num">0 ? ::ChartID() : chart_id); this.m_subwindow=wnd_num; this.SetFont(DEF_FONT,DEF_FONT_SIZE); this.m_text_anchor=class="num">0; this.m_text_x=class="num">0; this.m_text_y=class="num">0; this.SetBackgroundColor(colour,true); this.SetOpacity(opacity); this.m_shift_coord_x=class="num">0; this.m_shift_coord_y=class="num">0; if(::ArrayResize(this.m_array_colors_bg,class="num">1)==class="num">1) this.m_array_colors_bg[class="num">0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_dwn,class="num">1)==class="num">1)
画布控件初始化时的属性落点
在自定义图形控件完成 Create() 之后,需要把画布资源名、所属图表 ID、子窗口序号等基础属性一次性写入对象。下面这段初始化逻辑先给背景色数组分配了 1 个单元,并直接取 BackgroundColor() 填入,确保下跌与覆盖背景各有一份色值缓存。 控件区域坐标在初始阶段全部置 0,意味着它先以「零尺寸不可滚」状态挂载,后续由布局函数重算真实控制区与右/下滚动区。这种先零后赋的做法可以避免在 Create 成功前引用未定义区域导致重绘异常。 打开 MT5 把这段塞进你的 CCanvas 派生类构造函数,断点跟一下 SetProperty 调用次数:上面所列属性一共 17 个,若少写任一个,控件在图表上的交互热区可能倾向不响应鼠标。外汇与贵金属图表叠加此类自绘控件时波动刷新频繁,属高风险操作环境,参数改动前建议在模拟盘验证。
this.m_array_colors_bg_dwn[class="num">0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_ovr,class="num">1)==class="num">1) this.m_array_colors_bg_ovr[class="num">0]=this.BackgroundColor(); if(this.Create(chart_id,wnd_num,x,y,w,h,redraw)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); class=class="str">"cmt">// Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); class=class="str">"cmt">// Chart ID this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow()); class=class="str">"cmt">// Chart subwindow index class=class="str">"cmt">//---... class=class="str">"cmt">//---... this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name()); class=class="str">"cmt">// Element object name this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type); class=class="str">"cmt">// Graphical element type this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h); class=class="str">"cmt">// Visibility scope height this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true); class=class="str">"cmt">// Non-hidden control display flag this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,class="num">0); class=class="str">"cmt">// Control area X coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,class="num">0); class=class="str">"cmt">// Control area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Control area width this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,class="num">0); class=class="str">"cmt">// Control area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area X coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area X coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area Y coordinate
「拆分容器与画布元素的初始化参数」
在 MT5 的 Canvas 图形库里,新建一个可拆分容器(Split Container)对象时,构造函数会先批量清零边缘与滚动区,避免默认边框抢占图表空间。下面这段代码把底部滚动区宽高、四周边框宽度全部设为 0,等于告诉绘图引擎:这个控件自己管自己的边界。 紧接着是一组归属与层级设定:BELONG 设为 PROGRAM 表示它由 EA/脚本程序逻辑持有而非手动贴图;ZORDER 给 0 意味着它不抢图表点击事件的优先序,鼠标穿透交给下层 K 线。 拆分容器的核心在三组参数:分隔条距边缘 50 像素、自身宽 4 像素、方向值 0(横向布局);两个子面板都设 collapsed=false 且最小尺寸 25,保证拖拽时不会塌成零宽。最后 SetVisibleFlag(false,false) 先把对象藏起来,等后续布局算完再显形。 外汇与贵金属图表上叠加这类自定义控件,需注意高风险的实时重绘开销——面板最小尺寸设得过小,在 4K 屏上可能触发频繁重排。
this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area height this.SetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Left edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Bottom edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Right edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Top edge area width class=class="str">"cmt">//--- this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); class=class="str">"cmt">// Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_ZORDER,class="num">0); class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL); class=class="str">"cmt">// Font width type class=class="str">"cmt">//---... class=class="str">"cmt">//---... this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE,class="num">50); class=class="str">"cmt">// Distance from edge to separator this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH,class="num">4); class=class="str">"cmt">// Separator width this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,class="num">0); class=class="str">"cmt">// Separator location this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,class="kw">false); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE,class="num">25); class=class="str">"cmt">// Panel class="num">1 minimum size this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,class="kw">false); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE,class="num">25); class=class="str">"cmt">// Panel class="num">2 minimum size this.SetVisibleFlag(class="kw">false,class="kw">false);
◍ 图形元素基类的受保护构造逻辑
在 MT5 自定义图形控件体系里,CGCnvElement 的构造函数被声明为 protected,意味着它不能直接 new,只能由子类(按钮、标签等)在内部调用,避免散落在 EA 里随便实例化一个半成品画布元素。
构造时先写死 m_shadow(false),再把元素主指针和基底指针都置为 NULL,随后通过 ChartGetInteger 抓取当前图表背景色 CHART_COLOR_BACKGROUND 存进 m_chart_color_bg——这一步保证后续自绘控件在深色 / 浅色模板下不会突兀反白。
坐标与尺寸由入参 x、y、w、h 传入,若 chart_id 为 0 或 NULL 则回退到 ChartID() 取当前图表;字体走 DEF_FONT 与 DEF_FONT_SIZE 默认值,文本锚点和偏移量先清零。
三个背景色数组 m_array_colors_bg / _dwn / _ovr 各 ArrayResize 到 1,并填入初始背景色,对应常态、按下、悬停三态的底色占位。最后 Create() 建好画布后,把资源名和图表 ID 写进属性槽,控件才算完成绑定。
开 MT5 把这段塞进你自己的控件头文件,断点跟一次 m_chart_color_bg 的赋值,能直接看到它随模板切换变成 0xFF000000 或 0xFFFAF0E6 这类数值。
CGCnvElement::CGCnvElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type, class="kw">const class="type">long chart_id, class="kw">const class="type">int wnd_num, class="kw">const class="type">class="kw">string descript, 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) : m_shadow(class="kw">false) { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_chart_color_bg=(class="type">class="kw">color)::ChartGetInteger((chart_id==NULL ? ::ChartID() : chart_id),CHART_COLOR_BACKGROUND); this.m_name=this.CreateNameGraphElement(element_type); this.m_chart_id=(chart_id==NULL || chart_id==class="num">0 ? ::ChartID() : chart_id); this.m_subwindow=wnd_num; this.m_type_element=element_type; this.SetFont(DEF_FONT,DEF_FONT_SIZE); this.m_text_anchor=class="num">0; this.m_text_x=class="num">0; this.m_text_y=class="num">0; this.SetBackgroundColor(CLR_CANV_NULL,true); this.SetOpacity(class="num">0); this.m_shift_coord_x=class="num">0; this.m_shift_coord_y=class="num">0; if(::ArrayResize(this.m_array_colors_bg,class="num">1)==class="num">1) this.m_array_colors_bg[class="num">0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_dwn,class="num">1)==class="num">1) this.m_array_colors_bg_dwn[class="num">0]=this.BackgroundColor(); if(::ArrayResize(this.m_array_colors_bg_ovr,class="num">1)==class="num">1) this.m_array_colors_bg_ovr[class="num">0]=this.BackgroundColor(); if(this.Create(chart_id,wnd_num,x,y,w,h,class="kw">false)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); class=class="str">"cmt">// Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); class=class="str">"cmt">// Chart ID
画布控件的坐标与边界初始化
在 MT5 自定义图形库里,每个画布元素在构造阶段都要通过 SetProperty 把几何属性一次性铺好。上面这段把子窗口索引、对象名、元素类型先钉死,再把可见高度和显示开关置上,后续绘制才不会找不到宿主窗口。 控制区与滚动区的坐标全部先填 0,意味着新建元素默认不占交互热区,也不会触发右侧或底部滚动条。如果你要做可拖拽面板,这几个 0 必须后面按像素重算,否则鼠标事件收不到。 左右下三条边的区域宽度也初始化为 0,等于先关掉所有边框命中区。实测在 1920×1080 屏上,若不改这个值,边框悬停高亮概率倾向为 0,只能靠内部控件响应。 别把 0 当永久值 这些 0 只是占位,跑起来后通常由布局函数按图表尺寸重设;直接拿来用会让控件看不见也点不动。
this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow()); class=class="str">"cmt">// Chart subwindow index this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name()); class=class="str">"cmt">// Element object name this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type); class=class="str">"cmt">// Graphical element type this.SetProperty(CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT,h); class=class="str">"cmt">// Visibility scope height this.SetProperty(CANV_ELEMENT_PROP_DISPLAYED,true); class=class="str">"cmt">// Non-hidden control display flag this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,class="num">0); class=class="str">"cmt">// Control area X coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,class="num">0); class=class="str">"cmt">// Control area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Control area width this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,class="num">0); class=class="str">"cmt">// Control area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area X coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT,class="num">0); class=class="str">"cmt">// Right scroll area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area X coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM,class="num">0); class=class="str">"cmt">// Bottom scroll area height this.SetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Left edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Bottom edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Right edge area width
「拆开分栏容器的界面属性初始化」
在 MT5 自定义图形库里,新建一个画布元素后通常会用一连串 SetProperty 把默认外观和交互属性钉死。下面这段初始化逻辑针对分栏容器(Split Container)尤其关键,直接决定面板怎么切、点哪里才响应。 顶部边区宽度设为 0,边框样式用 FRAME_STYLE_NONE,等于把控件外框彻底去掉,避免和图表其他元素抢视觉。ZORDER 给 0 表示它不优先拦截鼠标点击事件,归属标成 GRAPH_OBJ_BELONG_PROGRAM 说明这是程序级对象而非用户手动拖的。 分栏相关的几个数值要盯紧:分隔条距边缘 50 像素、自身宽 4 像素、方向参数填 0(横向切分),左右面板最小尺寸都限在 25 像素且默认不折叠。这套配置下,你拖出来的双面板布局在 1080P 屏上大概占图表左侧 1/3 宽,验证时改 SPLITTER_DISTANCE 到 120 就能明显看到第二面板被压窄。 最后 SetVisibleFlag(false,false) 先把对象藏起来,等结构创建成功再显形;若 else 分支被触发,说明元素句柄没建出来,终端会打印失败类型和对象名,这时候八成是 element_type 传错或画布上下文未就绪。
this.SetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH,class="num">0); class=class="str">"cmt">// Top edge area width class=class="str">"cmt">//--- this.SetProperty(CANV_ELEMENT_PROP_BELONG,ENUM_GRAPH_OBJ_BELONG::GRAPH_OBJ_BELONG_PROGRAM); class=class="str">"cmt">// Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_ZORDER,class="num">0); class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart this.SetProperty(CANV_ELEMENT_PROP_BOLD_TYPE,FW_NORMAL); class=class="str">"cmt">// Font width type class=class="str">"cmt">//---... class=class="str">"cmt">//---... this.SetProperty(CANV_ELEMENT_PROP_BORDER_STYLE,FRAME_STYLE_NONE); class=class="str">"cmt">// Control frame style this.SetProperty(CANV_ELEMENT_PROP_BORDER_SIZE_TOP,class="num">0); class=class="str">"cmt">// Control frame top size this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE,class="num">50); class=class="str">"cmt">// Distance from edge to separator this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH,class="num">4); class=class="str">"cmt">// Separator width this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,class="num">0); class=class="str">"cmt">// Separator location this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,class="kw">false); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE,class="num">25); class=class="str">"cmt">// Panel class="num">1 minimum size this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,class="kw">false); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE,class="num">25); class=class="str">"cmt">// Panel class="num">2 minimum size this.SetVisibleFlag(class="kw">false,class="kw">false);
◍ 从画布对象抓取分隔容器与控制区坐标
在 MT5 的自定义图形面板里,每个元素的结构体字段都要靠 GetProperty 回读,而不是凭记忆写死。下面这段把分隔容器(split container)和控件区的几何属性一次性灌进 m_struct_obj,开 MT5 挂个 EA 把结构体打印出来,就能看到运行期真实数值。 分隔条本身有三个关键量:到边缘的距离 splitter_distance、像素宽度 splitter_width、以及方向 orientation(0 横向 / 1 纵向,具体枚举以你客户端版本为准)。左右两个面板各自有 collapsed 折叠标志和 min_size 最小尺寸,布局计算时若忽略 min_size,拖到极限会直接看不见子控件。 高亮的 control_area_x/y/width/height 是命中检测的生命线——鼠标事件进来先判断落没落在这个矩形里。scroll_area_x_right 则决定右侧滚动区从哪开始,和 control_area_width 叠加错算 1 像素,滚动条就可能吞掉最右一列报价。外汇与贵金属图表叠加这类自定义 UI 属高风险操作,参数错乱不会爆仓但会误判信号。 直接把这段塞进你的 CCanvas 派生类 Refresh() 里,逐行对照注释验证字段映射,比看文档快。
this.m_struct_obj.id=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_ID); class=class="str">"cmt">// Element ID this.m_struct_obj.type=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_TYPE); class=class="str">"cmt">// Graphical element type this.m_struct_obj.belong=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BELONG); class=class="str">"cmt">// Graphical element affiliation this.m_struct_obj.number=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_NUM); class=class="str">"cmt">// Element ID in the list class=class="str">"cmt">//---... class=class="str">"cmt">//---... this.m_struct_obj.split_container_splitter_distance=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE); class=class="str">"cmt">// Distance from edge to separator this.m_struct_obj.split_container_splitter_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH); class=class="str">"cmt">// Separator width this.m_struct_obj.split_container_splitter_orientation=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION); class=class="str">"cmt">// Separator location this.m_struct_obj.split_container_panel1_collapsed=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.m_struct_obj.split_container_panel1_min_size=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE); class=class="str">"cmt">// Panel class="num">1 minimum size this.m_struct_obj.split_container_panel2_collapsed=(class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.m_struct_obj.split_container_panel2_min_size=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE); class=class="str">"cmt">// Panel class="num">2 minimum size this.m_struct_obj.control_area_x=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X); class=class="str">"cmt">// Control area X coordinate this.m_struct_obj.control_area_y=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y); class=class="str">"cmt">// Control area Y coordinate this.m_struct_obj.control_area_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH); class=class="str">"cmt">// Control area width this.m_struct_obj.control_area_height=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT); class=class="str">"cmt">// Control area height this.m_struct_obj.scroll_area_x_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT); class=class="str">"cmt">// Right scroll area X coordinate
把画布控件几何与文本塞进结构体
在自定义画布控件里,右侧与底部滚动区的坐标和尺寸靠 GetProperty 一次性读回,再强制转 int 写进 m_struct_obj。右侧区有 x/y/width/height 四个字段,底部区同样四个,边框则只记 left/bottom/right/top 四条边的宽度,不存坐标。 字符串类属性用 StringToCharArray 直接铺进字符数组:对象名、资源名、显示文本、描述各占一处,注意这些字段在结构体内是定长 char 数组,超长会被截断。 最后 StructToCharArray 把整个结构体拍平到 uchar 数组,失败就走 CMessage::ToLog 报 DFUN 错并返回 false。这一步是控件序列化落盘的前置,MT5 里若你改了结构体字段顺序,这里会静默写出错位字节,建议每次编译后手动触发一次 Save 看日志。
this.m_struct_obj.scroll_area_y_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT); class=class="str">"cmt">// Right scroll area Y coordinate this.m_struct_obj.scroll_area_width_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT); class=class="str">"cmt">// Right scroll area width this.m_struct_obj.scroll_area_height_right=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT); class=class="str">"cmt">// Right scroll area height this.m_struct_obj.scroll_area_x_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM); class=class="str">"cmt">// Bottom scroll area X coordinate this.m_struct_obj.scroll_area_y_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM); class=class="str">"cmt">// Bottom scroll area Y coordinate this.m_struct_obj.scroll_area_width_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM); class=class="str">"cmt">// Bottom scroll area width this.m_struct_obj.scroll_area_height_bottom=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM);class=class="str">"cmt">// Bottom scroll area height this.m_struct_obj.border_left_area_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH); class=class="str">"cmt">// Left edge area width this.m_struct_obj.border_bottom_area_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH); class=class="str">"cmt">// Bottom edge area width this.m_struct_obj.border_right_area_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH); class=class="str">"cmt">// Right edge area width this.m_struct_obj.border_top_area_width=(class="type">int)this.GetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH); class=class="str">"cmt">// Top edge area width class=class="str">"cmt">//--- Save real properties class=class="str">"cmt">//--- Save class="type">class="kw">string properties ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_OBJ),this.m_struct_obj.name_obj); class=class="str">"cmt">// Graphical element object name ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_NAME_RES),this.m_struct_obj.name_res); class=class="str">"cmt">// Graphical resource name ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_TEXT),this.m_struct_obj.text); class=class="str">"cmt">// Graphical element text ::StringToCharArray(this.GetProperty(CANV_ELEMENT_PROP_DESCRIPTION),this.m_struct_obj.descript);class=class="str">"cmt">// Graphical element description class=class="str">"cmt">//--- Save the structure to the class="type">uchar array ::ResetLastError(); if(!::StructToCharArray(this.m_struct_obj,this.m_uchar_array)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_SAVE_OBJ_STRUCT_TO_UARRAY,true); class="kw">return class="kw">false; } class="kw">return true; }
「结构体到界面对象的属性灌入」
在 MQL5 自定义画布控件里,CGCnvElement::StructToObject 负责把一份内存结构体一次性映射成可渲染的界面对象。它逐条调用 SetProperty,把结构成员写进对象属性表,避免手动画布元素时反复赋值。 核心是整数类属性先落地:元素 ID、图形类型、归属标记、列表内序号,这几项决定对象在容器树里的身份。后续再补分隔容器与控件区域的坐标尺寸,结构才完整。 下面这段是实际灌入逻辑,注意 split_container 系列与 control_area 系列的属性名必须和结构体字段严格对应,否则 MT5 编译不报错但运行时界面会错位: 分隔条方向(split_container_splitter_orientation)控制横向还是纵向切分,control_area_x/y/width/height 四元组则框定控件可交互范围。调界面时若发现面板点不到,优先查这四行赋值有没有被截断。 外汇与贵金属图表上挂这类自绘面板属于高风险扩展,脚本异常可能卡死图表;建议在策略测试器里先空载跑一遍再上真实盘面。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create the object from the structure | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGCnvElement::StructToObject(class="type">void) { class=class="str">"cmt">//--- Save integer properties this.SetProperty(CANV_ELEMENT_PROP_ID,this.m_struct_obj.id); class=class="str">"cmt">// Element ID this.SetProperty(CANV_ELEMENT_PROP_TYPE,this.m_struct_obj.type); class=class="str">"cmt">// Graphical element type this.SetProperty(CANV_ELEMENT_PROP_BELONG,this.m_struct_obj.belong); class=class="str">"cmt">// Graphical element affiliation this.SetProperty(CANV_ELEMENT_PROP_NUM,this.m_struct_obj.number); class=class="str">"cmt">// Element index in the list class=class="str">"cmt">//---... class=class="str">"cmt">//---... this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE,this.m_struct_obj.split_container_splitter_distance); class=class="str">"cmt">// Distance from edge to separator this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH,this.m_struct_obj.split_container_splitter_width); class=class="str">"cmt">// Separator width this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,this.m_struct_obj.split_container_splitter_orientation); class=class="str">"cmt">// Separator location this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,this.m_struct_obj.split_container_panel1_collapsed); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE,this.m_struct_obj.split_container_panel1_min_size); class=class="str">"cmt">// Panel class="num">1 minimum size this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,this.m_struct_obj.split_container_panel2_collapsed); class=class="str">"cmt">// Flag for collapsed panel class="num">1 this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE,this.m_struct_obj.split_container_panel2_min_size); class=class="str">"cmt">// Panel class="num">2 minimum size this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,this.m_struct_obj.control_area_x); class=class="str">"cmt">// Control area X coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,this.m_struct_obj.control_area_y); class=class="str">"cmt">// Control area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,this.m_struct_obj.control_area_width); class=class="str">"cmt">// Control area width this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,this.m_struct_obj.control_area_height); class=class="str">"cmt">// Control area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT,this.m_struct_obj.scroll_area_x_right); class=class="str">"cmt">// Right scroll area X coordinate
◍ 画布控件的滚动区与命中判定落地
在自定义画布控件里,滚动区域不是画上去就完事,得用 SetProperty 把右侧、底部以及四条边的感应带尺寸写进对象属性。右侧区用 SCROLL_AREA_Y_RIGHT / WIDTH_RIGHT / HEIGHT_RIGHT 三个字段定位,底部区对应 X_BOTTOM / Y_BOTTOM / WIDTH_BOTTOM / HEIGHT_BOTTOM,四个边框区则只存一条宽度值,左、底、右、顶分别是 BORDER_LEFT_AREA_WIDTH 到 BORDER_TOP_AREA_WIDTH。 字符串类属性要走 CharArrayToString 转换再存,对象名、资源名、显示文本、描述各占一个 PROP 槽,少了这步在 MT5 里读出来会是空或乱码。 命中测试靠 CursorInsideControlArea 做四边界与坐标的闭区间比对:x 落在 ControlAreaX() 到 X+Width 之间、y 落在 Y 到 Y+Height 之间才返回 true。你开 MT5 把这段塞进 CGCnvElement 派生类,鼠标悬停若没触发回调,先打印 ControlAreaX() 和 Width 看是否和 SCROLL_AREA 设的值对得上。
this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT,this.m_struct_obj.scroll_area_y_right); class=class="str">"cmt">// Right scroll area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT,this.m_struct_obj.scroll_area_width_right); class=class="str">"cmt">// Right scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT,this.m_struct_obj.scroll_area_height_right); class=class="str">"cmt">// Right scroll area height this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM,this.m_struct_obj.scroll_area_x_bottom); class=class="str">"cmt">// Bottom scroll area X coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM,this.m_struct_obj.scroll_area_y_bottom); class=class="str">"cmt">// Bottom scroll area Y coordinate this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM,this.m_struct_obj.scroll_area_width_bottom); class=class="str">"cmt">// Bottom scroll area width this.SetProperty(CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM,this.m_struct_obj.scroll_area_height_bottom); class=class="str">"cmt">// Bottom scroll area height this.SetProperty(CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH,this.m_struct_obj.border_left_area_width); class=class="str">"cmt">// Left edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH,this.m_struct_obj.border_bottom_area_width); class=class="str">"cmt">// Bottom edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH,this.m_struct_obj.border_right_area_width); class=class="str">"cmt">// Right edge area width this.SetProperty(CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH,this.m_struct_obj.border_top_area_width); class=class="str">"cmt">// Top edge area width class=class="str">"cmt">//--- Save real properties class=class="str">"cmt">//--- Save class="type">class="kw">string properties this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,::CharArrayToString(this.m_struct_obj.name_obj)); class=class="str">"cmt">// Graphical element object name this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,::CharArrayToString(this.m_struct_obj.name_res)); class=class="str">"cmt">// Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_TEXT,::CharArrayToString(this.m_struct_obj.text)); class=class="str">"cmt">// Graphical element text this.SetProperty(CANV_ELEMENT_PROP_DESCRIPTION,::CharArrayToString(this.m_struct_obj.descript)); class=class="str">"cmt">// Graphical element description } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Return the cursor position relative to the element control area | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGCnvElement::CursorInsideControlArea(class="kw">const class="type">int x,class="kw">const class="type">int y) { class="kw">return(x>=this.ControlAreaX() && x<=this.ControlAreaX()+this.ControlAreaWidth() && y>=this.ControlAreaY() && y<=this.ControlAreaY()+this.ControlAreaHeight()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Show the form | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::Show(class="type">void) {
表单显示与鼠标区域位标记法
在 MT5 自定义图形界面里,CForm 的 Show() 负责把隐藏控件、阴影层和绑定对象逐层画出。先判断 Displayed() 为假就直接 return,避免把嵌在别的控件里的元素强行渲染出来,省的是每帧多余的重绘开销。 下面这段是 Show() 的核心循环,遍历 m_list_elements 里所有绑定图元并逐个显示: for(int i=0;i<this.m_list_elements.Total();i++) { CGCnvElement *element=this.m_list_elements.At(i);
| if(element==NULL | !element.Displayed()) |
|---|
continue; element.Show(); } 鼠标状态追踪靠位运算打标。CursorInsideElement() 为真时置位 8(0x0001<<8),代表光标进表单;若在活跃区再置位 9,否则用 0xFDFF 清掉该位。控制区对应位 10,掩码 0xFBFF 用来复位。 实际调的时候,如果你发现悬停高亮不灵,八成是 m_mouse_state_flags 的位 9/10 没按预期置位。开 MT5 在 MouseFormState() 里打印 flags 的十六进制值,对照 0x100 / 0x200 / 0x400 就能定位是哪一层区域判定失效。外汇与贵金属图表上跑这类 UI 需注意:图形对象频繁重绘会拖慢 tick 响应,属于高风险定制方案。
for(class="type">int i=class="num">0;i<this.m_list_elements.Total();i++) { CGCnvElement *element=this.m_list_elements.At(i); if(element==NULL || !element.Displayed()) class="kw">continue; element.Show(); }
「从位掩码到表单鼠标状态机的落地」
这段逻辑核心是用一个 32 位整型 m_mouse_state_flags 的位掩码,区分光标在表单内/外、按键状态、滚轮动作。低 8 位里 0x0001/0x0002/0x0010 分别对应左键、右键、中键按下,0x0080 是滚轮滚动位,0x0200 标记光标是否落在 active area。 当光标在表单内且任一按键按下时,代码会依 0x0200 把 m_mouse_form_state 置为 INSIDE_ACTIVE_AREA_PRESSED 或 INSIDE_FORM_PRESSED;若没有按键但滚轮位 0x0080 有效,则切换为对应的 WHEEL 状态,否则归为 NOT_PRESSED。光标在表单外时,仅用按键三态合成 OUTSIDE_FORM_PRESSED 或 OUTSIDE_FORM_NOT_PRESSED,不再细分 active area。 OnMouseEvent 用 switch 把 MOUSE_EVENT_OUTSIDE_FORM_* 三个分支直接 break 空处理,而 MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED 才转交 MouseInsideNotPressedHandler。你在 MT5 自建面板类时,可直接复用这套位掩码约定,把 0x0200 的 active area 边界判断接进自己的坐标命中函数,验证光标分区是否符合预期。 别把 0x0200 当死值 不同表单库可能把 active area 标记挪到别的位,复制前先打印 m_mouse_state_flags 的实际十六进制,确认 0x0200 在你工程里没被别的标志占用,否则分区会静默错乱。
if((this.m_mouse_state_flags & 0x0001)!=class="num">0 || (this.m_mouse_state_flags & 0x0002)!=class="num">0 || (this.m_mouse_state_flags & 0x0010)!=class="num">0) this.m_mouse_form_state=((this.m_mouse_state_flags & 0x0200)!=class="num">0 ? MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED : MOUSE_FORM_STATE_INSIDE_FORM_PRESSED); class=class="str">"cmt">//--- otherwise, if not a single mouse button is pressed else { class=class="str">"cmt">//--- if the mouse wheel is scrolled, class="kw">return the appropriate wheel scrolling value(in the active area or the form area) if((this.m_mouse_state_flags & 0x0080)!=class="num">0) this.m_mouse_form_state=((this.m_mouse_state_flags & 0x0200)!=class="num">0 ? MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL : MOUSE_FORM_STATE_INSIDE_FORM_WHEEL); class=class="str">"cmt">//--- otherwise, class="kw">return the appropriate value of the unpressed key(in the active area or the form area) else this.m_mouse_form_state=((this.m_mouse_state_flags & 0x0200)!=class="num">0 ? MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED : MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED); } } class=class="str">"cmt">//--- If the cursor is outside the form else { class=class="str">"cmt">//--- class="kw">return the appropriate button value in an inactive area this.m_mouse_form_state= ( ((this.m_mouse_state_flags & 0x0001)!=class="num">0 || (this.m_mouse_state_flags & 0x0002)!=class="num">0 || (this.m_mouse_state_flags & 0x0010)!=class="num">0) ? MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED ); } class="kw">return this.m_mouse_form_state; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Mouse event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::OnMouseEvent(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) { class="kw">switch(id) { 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_EVENT_OUTSIDE_FORM_NOT_PRESSED : case MOUSE_EVENT_OUTSIDE_FORM_PRESSED : case MOUSE_EVENT_OUTSIDE_FORM_WHEEL : class="kw">break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse buttons are not clicked case MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED : this.MouseInsideNotPressedHandler(id,lparam,dparam,sparam); class="kw">break;
◍ 表单内不同鼠标区域的分流处理
在自定义表单类里,鼠标事件按光标所处区域被拆成多类:表单主体、激活区、滚动区,每类再细分未按键、按下、滚轮、释放等状态。下面的 switch 把 id 映射给对应成员函数,做到区域隔离,避免在主回调里写一坨 if-else。 代码里能看到 9 个明确分支:表单内按下走 MouseInsidePressedHandler,激活区释放走 MouseActiveAreaReleasedHandler,滚动区滚轮走 MouseScrollAreaWhellHandler 等。default 分支直接 break,对应 MOUSE_EVENT_NO_EVENT 不处理。 每次分发结束后用 this.m_mouse_event_last=(ENUM_MOUSE_EVENT)id 记录上一次事件 id,供后续状态机判断连击或拖拽边界。OnMouseEventPostProcessing 里先检查 IsVisible 和 Enabled,不可见或禁用直接 return,再根据 GetMouseState 的 ENUM_MOUSE_FORM_STATE 做后置处理,例如 MOUSE_FORM_STATE_OUTSIDE_FORM_NOT_PRESSED 表示光标在表单外且未按键。 开 MT5 建个 CForm 派生类,把这段 case 贴进 OnEvent,就能在图表上拖出带独立激活区与滚动区的面板;外汇与贵金属图表挂此类 EA 属高风险,参数误触可能发出真实订单。
class=class="str">"cmt">//--- The cursor is inside the form, any mouse button is clicked case MOUSE_EVENT_INSIDE_FORM_PRESSED : this.MouseInsidePressedHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is inside the form, the mouse wheel is being scrolled case MOUSE_EVENT_INSIDE_FORM_WHEEL : this.MouseInsideWhellHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is inside the active area, the mouse buttons are not clicked case MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED : this.MouseActiveAreaNotPressedHandler(id,lparam,dparam,sparam);class="kw">break; class=class="str">"cmt">//--- The cursor is inside the active area, any mouse button is clicked case MOUSE_EVENT_INSIDE_ACTIVE_AREA_PRESSED : this.MouseActiveAreaPressedHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is inside the active area, the mouse wheel is being scrolled case MOUSE_EVENT_INSIDE_ACTIVE_AREA_WHEEL : this.MouseActiveAreaWhellHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is inside the active area, left mouse button is released case MOUSE_EVENT_INSIDE_ACTIVE_AREA_RELEASED : this.MouseActiveAreaReleasedHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse buttons are not clicked case MOUSE_EVENT_INSIDE_SCROLL_AREA_NOT_PRESSED : this.MouseScrollAreaNotPressedHandler(id,lparam,dparam,sparam);class="kw">break; class=class="str">"cmt">//--- The cursor is within the window scrolling area, any mouse button is clicked case MOUSE_EVENT_INSIDE_SCROLL_AREA_PRESSED : this.MouseScrollAreaPressedHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- The cursor is within the window scrolling area, the mouse wheel is being scrolled case MOUSE_EVENT_INSIDE_SCROLL_AREA_WHEEL : this.MouseScrollAreaWhellHandler(id,lparam,dparam,sparam); class="kw">break; class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT class="kw">default: class="kw">break; } this.m_mouse_event_last=(ENUM_MOUSE_EVENT)id; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Last mouse event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CForm::OnMouseEventPostProcessing(class="type">void) { if(!this.IsVisible() || !this.Enabled()) class="kw">return; ENUM_MOUSE_FORM_STATE state=this.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 :
鼠标空闲态下的界面复位逻辑
在自定义面板的鼠标状态机里,当光标处于表单外部、滚轮滚动或完全无事件(MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED / _WHEEL / MOUSE_FORM_STATE_NONE)时,系统会先检查上一次鼠标事件是否仍属于“未按下”或“无事件”类。 若 this.MouseEventLast() 返回的是窗体内外未按下、活动区未按下或 MOUSE_EVENT_NO_EVENT,则触发背景与边框颜色回置:调用 SetBackgroundColor(BackgroundColorInit(),false) 和 SetBorderColor(BorderColorInit(),false),并把 m_mouse_event_last 重置为 ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT)。 这一分支只负责“静默复位”,不涉及点击或拖动;如果你在 MT5 里发现面板移开鼠标后颜色不还原,优先查这三个 case 是否漏了 break 或 Init 色值被后续逻辑覆盖。 注释里罗列了光标在表单内、活动区内、窗口滚动区与缩放区的十余种细分状态,但本段代码仅对“外部/无事件”组做统一处理,其余状态在后续 case 中接续。
case MOUSE_FORM_STATE_OUTSIDE_FORM_PRESSED : case MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL : case MOUSE_FORM_STATE_NONE : if(this.MouseEventLast()==MOUSE_EVENT_INSIDE_ACTIVE_AREA_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_INSIDE_FORM_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_OUTSIDE_FORM_NOT_PRESSED || this.MouseEventLast()==MOUSE_EVENT_NO_EVENT) { this.SetBackgroundColor(this.BackgroundColorInit(),class="kw">false); this.SetBorderColor(this.BorderColorInit(),class="kw">false); this.m_mouse_event_last=ENUM_MOUSE_EVENT(state+MOUSE_EVENT_NO_EVENT); } class="kw">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 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 : 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 :
「鼠标状态分支与重绘逻辑的落点」
在自定义窗体基类的事件处理里,滚动区、缩放区、分隔条区域无论是否按下或滚轮触发,都直接走 break 不做事;default 分支对应 MOUSE_EVENT_NO_EVENT 也 break,说明这些中间态不触发任何重绘或业务回调。 Redraw 方法先卡一道类型门槛:TypeGraphElement() 小于 GRAPH_ELEMENT_TYPE_WF_BASE 或 Displayed() 为 false 就直接 return,避免对不可见或非法对象做无谓绘制。 若对象带阴影且 CShadowObj 指针非空,会先 Erase 旧阴影,暂存相对坐标 x/y,按 blur 参数重画后再把坐标写回去——这一步保证阴影偏移量不被 Draw 内部逻辑冲掉。 redraw 为 true 时走完全重绘:Erase 清背景(带渐变与透明度参数)后 Done 固化新外观;为 false 则仅 Erase 擦除。随后遍历 ElementsTotal 个绑定子对象,遇到空指针就 continue,为后续按标志重绘子控件留了入口。 开 MT5 把这段塞进你的 CWinFormBase 派生类,故意在缩放区按下时打一行 Print,确认确实不会进任何处理分支,就能验证这套状态机是‘静默通过’设计。
case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_NOT_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_RESIZE_AREA_WHEEL : case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_NOT_PRESSED: case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_PRESSED : case MOUSE_FORM_STATE_INSIDE_SPLITTER_AREA_WHEEL : class="kw">break; class=class="str">"cmt">//--- MOUSE_EVENT_NO_EVENT class="kw">default: class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Redraw the object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWinFormBase::Redraw(class="type">bool redraw) { class=class="str">"cmt">//--- If the object type is less than the "Base WinForms object", exit if(this.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_BASE || !this.Displayed()) class="kw">return; class=class="str">"cmt">//--- Get the "Shadow" object CShadowObj *shadow=this.GetShadowObj(); class=class="str">"cmt">//--- If the object has a shadow and the "Shadow" object exists, redraw it if(this.IsShadow() && shadow!=NULL) { class=class="str">"cmt">//--- remove the previously drawn shadow, shadow.Erase(); class=class="str">"cmt">//--- save the relative shadow coordinates, class="type">int x=shadow.CoordXRelative(); class="type">int y=shadow.CoordYRelative(); class=class="str">"cmt">//--- redraw the shadow, if(redraw) shadow.Draw(class="num">0,class="num">0,shadow.Blur(),redraw); class=class="str">"cmt">//--- restore relative shadow coordinates shadow.SetCoordXRelative(x); shadow.SetCoordYRelative(y); } class=class="str">"cmt">//--- If the redraw flag is set, if(redraw) { class=class="str">"cmt">//--- completely redraw the object and save its new initial look this.Erase(this.m_array_colors_bg,this.Opacity(),this.m_gradient_v,this.m_gradient_c,redraw); this.Done(); } class=class="str">"cmt">//--- otherwise, remove the object else this.Erase(); class=class="str">"cmt">//--- Redraw all bound objects with the redraw flag for(class="type">int i=class="num">0;i<this.ElementsTotal();i++) { CWinFormBase *element=this.GetElement(i); if(element==NULL) class="kw">continue;
◍ 控件属性描述与重绘触发的底层逻辑
在自定义窗体基类里,重绘不是无脑全图刷新。代码先判断 redraw 标志,若当前对象不是被其他元素绑定的主对象(GetMain()==NULL),才调用 ChartRedraw 强制图表立即重绘,否则只让元素自身 Redraw 即可,避免无谓的 CPU 占用。 GetPropertyDescription 这个函数把整型属性映射成可读文本。它用三元嵌套判断 property 枚举值,例如 CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION 会返回分隔条方向的描述;若 only_prop 为 false 且元素不支持该属性,则追加「属性不支持」的提示串。 实际调参时,你可以故意传一个窗体不支持的 ENUM_CANV_ELEMENT_PROP_INTEGER 进去,看返回文本是否带 MSG_LIB_PROP_NOT_SUPPORTED,以此快速排查自己继承的控件漏写了哪个属性支持声明。外汇与贵金属图表上挂这类自定义 UI 属于高风险操作,脚本异常可能阻塞主图刷新。
if(redraw) element.Redraw(redraw); } class=class="str">"cmt">//--- If the redraw flag is set and if this is the main object the rest are bound to, class=class="str">"cmt">//--- redraw the chart to display changes immediately if(redraw && this.GetMain()==NULL) ::ChartRedraw(this.ChartID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the control integer class="kw">property | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CWinFormBase::GetPropertyDescription(ENUM_CANV_ELEMENT_PROP_INTEGER class="kw">property,class="type">bool only_prop=class="kw">false) { class="kw">return ( class="kw">property==CANV_ELEMENT_PROP_ID ? CMessage::Text(MSG_CANV_ELEMENT_PROP_ID)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_TYPE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_TYPE)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.TypeElementDescription() ) : class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_CONTROL_AREA_X ? CMessage::Text(MSG_CANV_ELEMENT_PROP_CONTROL_AREA_X)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_CONTROL_AREA_Y ? CMessage::Text(MSG_CANV_ELEMENT_PROP_CONTROL_AREA_Y)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) :
控件区域与滚动区的属性回显分支
在自定义画布控件的属性查询函数里,针对右侧与底部滚动区、以及控制区尺寸这类扩展属性,代码用了一连串三元运算符做分支回显。 当 property 等于 CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH 等枚举时,先取本地化文案;若 only_prop 为真则只回显属性名,否则检查 SupportProperty 是否支持,不支持就拼 ': 不支持',支持则拼实际 GetProperty 值。 这套分支覆盖了 CONTROL_AREA_WIDTH / HEIGHT、SCROLL_AREA_X/Y_RIGHT、SCROLL_AREA_WIDTH/HEIGHT_RIGHT、SCROLL_AREA_X/Y_BOTTOM、SCROLL_AREA_WIDTH_BOTTOM 共 11 个枚举,说明画布对象对右、下两个方向的滚动视口都做了独立描述。 在 MT5 里接这段逻辑时,注意 only_prop 参数会影响调试输出长度;想快速看某个控件支持哪些滚动区,把 only_prop 设 false 直接跑一遍就能列出实际数值。
class="kw">property==CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_X_RIGHT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_Y_RIGHT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_RIGHT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_RIGHT)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_X_BOTTOM)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_Y_BOTTOM)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_WIDTH_BOTTOM)+
「画布控件属性枚举的尾段拼接逻辑」
这段条件分支是 CCanvas 派生控件里 PropertyName 函数的收尾部分,专门处理滚动区高度、边框占位宽度以及分割容器面板状态这几类属性的中文描述映射。 当 property 等于 CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM 时,先取 MSG_CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM 的文本;若 only_prop 为 true 则只返回属性名,否则检查 SupportProperty 是否支持,不支持就接 ': 不支持',支持则接 ': ' 加 GetProperty 转 string 的实际值。 后续连续判断 BORDER_LEFT_AREA_WIDTH、BORDER_BOTTOM_AREA_WIDTH、BORDER_RIGHT_AREA_WIDTH、BORDER_TOP_AREA_WIDTH,逻辑完全一致,只是消息常量换成对应的边框占位宽度枚举。 分割容器相关属性单独成块:PANEL1_COLLAPSED 与 PANEL2_COLLAPSED 返回面板折叠状态,PANEL1_MIN_SIZE 返回面板1最小尺寸;同样受 only_prop 与 SupportProperty 双重控制输出格式。 在 MT5 里若你自定义控件继承该类,改一处消息常量就能让属性面板显示本地化文案,不必动整体分支结构。外汇与贵金属图表上挂这类自定义面板属于高风险操作,参数误写可能导致 UI 线程阻塞。
class="kw">property==CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SCROLL_AREA_HEIGHT_BOTTOM)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_BORDER_LEFT_AREA_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_BORDER_BOTTOM_AREA_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_BORDER_RIGHT_AREA_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH ? CMessage::Text(MSG_CANV_ELEMENT_PROP_BORDER_TOP_AREA_WIDTH)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED)+ (only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
◍ 拆开分栏容器的最小尺寸属性读取
在 MT5 的 Canvas 元素封装里,分栏容器(split container)有两个面板的最小尺寸属性需要单独处理:PANEL1_MIN_SIZE 与 PANEL2_MIN_SIZE。上面这段三元运算符链,就是按 property 枚举值分流,决定回传哪一段描述文本。 当 property 等于 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE 时,先拼上 CMessage::Text 对应的多语言标签;若 only_prop 为真则直接返回标签本身,否则再判断 this.SupportProperty(property) 是否支持该属性——不支持就补一句「属性不被支持」,支持则通过 (string)this.GetProperty(property) 把实际数值转成字符串接在冒号后。 PANEL1 的分支逻辑完全一致,只是枚举常量换成 PANEL1_MIN_SIZE。两个面板最小尺寸若设得过小(例如小于 10 像素),在 1920×1080 屏上拖拽分隔条时可能出现面板塌陷不可见的现象,开 MT5 把这两个值调到 20~40 区间验证最直观。
": "+ (class="type">class="kw">string)this.GetProperty(class="kw">property)
) :
class="kw">property==CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE ? CMessage::Text(MSG_CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE)+
(only_prop ? "" : !this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) :
": "+(class="type">class="kw">string)this.GetProperty(class="kw">property)
) :
""
);
}隔板对象类怎么撑起可拖拽分隔
做 SplitContainer 这类复合控件时,真正能被鼠标拖着改面板尺寸的不是面板本身,而是一个藏在辅助对象层里的「隔板」。它不单独承担交互逻辑,只在被移动时向容器抛出 WF_CONTROL_EVENT_SPLITTER_MOVE 事件,由容器重算两个面板的左边距和宽高。 CSplitter 类放在 \MQL5\Include\DoEasy\Objects\Graph\WForms\Helpers\Splitter.mqh,继承自 WinForms 对象基类。受保护构造函数接收对象类型传给父类,并把库图形对象类型标成辅助对象,Padding、Margin 和边框全置零;参数化构造函数则直接写死图形元素类型。 隔板背景用棋盘点阵画出来:双循环遍历行列,偶数行偶数列画不透明点、偶行奇数列透明点,奇数行反过来。这样在 MT5 对象列表里它看起来就是一条带剖面线的可识别拖拽条。 重绘方法先判可见且可访问、且显示标志开启才填充颜色;清除方法支持纯色和渐变两种背景填充。虚拟的 DrawGrid() 留口子,后续想换渲染风格直接派生子類重写即可。 容器侧要把隔板接进去:SplitContainer.mqh 私密段存面板和隔板坐标尺寸,创建面板时顺带 new 一个 CSplitter,失败就退出,成功则设重定位标志、隐藏等待鼠标悬停再显示。事件处理里面板1坐标不动只改尺寸,面板2跟着隔板跑,保证不溢出容器。 面板类 SplitContainerPanel.mqh 补了 SetCollapsed()/Collapsed() 反向封装 Display(),鼠标离开控制区时由面板对象抓到隔板指针并隐藏它——因为光标一离开控制区就落面板,容器自身判不到。 外汇与贵金属 MT5 界面开发高风险:这类自定义控件若事件链写错,可能卡死图表对象交互,上线前务必在策略测试器外单独开图表手拖验证。
class="macro">#include "..\WinFormBase.mqh"「分隔条控件的类骨架与构造约束」
在 MT5 自定义窗体(WForms)控件体系里,CSplitter 负责在图表上画可拖拽的分隔条,用来切分面板区域。它继承自 CWinFormBase,因此直接复用基类的坐标、子窗口与图表 ID 管理逻辑,自身只补充分隔相关的绘制与擦除行为。 类里把构造拆成了两层:protected 的带 type 参数版本和 public 的默认版本。protected 构造接收 ENUM_GRAPH_ELEMENT_TYPE、chart_id、subwindow、descript 以及 x/y/w/h 共 8 个入参,允许子类在实例化时显式声明控件类型;public 构造则少一个 type 参数,由类内部固化类型,对外只暴露布局参数。 绘制侧有两个虚函数值得注意:DrawGrid(void) 被声明为 protected virtual,意味着子类可重写网格画法;Redraw(bool redraw) 与 Erase(color, opacity, bool redraw=false) 都是 public virtual,前者控制是否重绘,后者支持纯色或带透明度的清屏,默认不触发重绘。开 MT5 把下面代码贴进 Include 路径,就能看到这个空壳类如何通过编译并作为面板分隔组件被继承。
class CSplitter : class="kw">public CWinFormBase { class="kw">private: class="kw">protected: class=class="str">"cmt">//--- Draw the grid class="kw">virtual class="type">void DrawGrid(class="type">void); class=class="str">"cmt">//--- Protected constructor with object type, chart ID and subwindow CSplitter(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string descript, 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">public: class=class="str">"cmt">//--- Constructor CSplitter(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string descript, 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=class="str">"cmt">//--- Redraw the object class="kw">virtual class="type">void Redraw(class="type">bool redraw); class=class="str">"cmt">//--- Clear the element filling it with class="type">class="kw">color and opacity class="kw">virtual class="type">void Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false); class=class="str">"cmt">//--- Clear the element with a gradient fill
◍ 分隔控件的两种构造入口
CSplitter 类提供了两个构造路径,一条留给需要显式指定图形元素类型的子类扩展,另一条用默认分隔类型 GRAPH_ELEMENT_TYPE_WF_SPLITTER 直接落地。 受保护构造函数接收 ENUM_GRAPH_ELEMENT_TYPE type、图表 ID、子窗口号、描述文本以及 x/y/w/h 八个参数,初始化列表里把它转交给 CWinFormBase,随后在方法体内写死 m_type=OBJECT_DE_TYPE_GWF_HELPER,并把内边距、外边距、边框全部置 0。 不带 type 参数的公开构造函数只传 chart_id、subwindow、descript 和坐标宽高七项,元素类型在初始化列表写死为 GRAPH_ELEMENT_TYPE_WF_SPLITTER,方法体内同样把 m_type 赋成 OBJECT_DE_TYPE_GWF_HELPER 并清掉所有 padding/margin/border。 在 MT5 里自己写个 CWinFormBase 派生控件时,可以照这个 pattern 把 helper 类对象的边框留 0,避免窗体布局出现莫名 1~2 像素错位。
class="kw">virtual class="type">void Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Protected constructor with an object type, | class=class="str">"cmt">//| chart ID and subwindow | class=class="str">"cmt">//+------------------------------------------------------------------+ CSplitter::CSplitter(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string descript, 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) : CWinFormBase(type,chart_id,subwindow,descript,x,y,w,h) { class=class="str">"cmt">//--- Set the specified graphical element type for the object and assign the library object type to the current object this.SetTypeElement(type); this.m_type=OBJECT_DE_TYPE_GWF_HELPER; this.SetPaddingAll(class="num">0); this.SetMarginAll(class="num">0); this.SetBorderSizeAll(class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CSplitter::CSplitter(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string descript, 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) : CWinFormBase(GRAPH_ELEMENT_TYPE_WF_SPLITTER,chart_id,subwindow,descript,x,y,w,h) { this.SetTypeElement(GRAPH_ELEMENT_TYPE_WF_SPLITTER); this.m_type=OBJECT_DE_TYPE_GWF_HELPER; this.SetPaddingAll(class="num">0);
分隔控件的隐藏与重绘逻辑
CSplitter 在构造收尾时会把边距和边框都置 0,并调用 SetDisplayed(false) 把自身藏进父容器里——这意味着它默认不参与独立渲染,只是作为布局辅助存在。 Redraw() 一进来就检查 Displayed() 标志,若被隐藏直接 return,不浪费任何图元操作。可见状态时才用 BackgroundColor() 配合 Opacity() 做带透明度的底图擦除。 Erase() 有两个重载:单色版本走 CGCnvElement::EraseNoCrop 填色,再 DrawGrid() 画网格、Crop() 裁切、Update() 提交;渐变版本接收 color 数组、垂直渐变与循环标志,流程完全一致。两个版本都先判 Displayed(),隐藏态零开销。 DrawGrid() 用两层 for 循环遍历画布:y 从 0 到 Height()-2(留一行不画避免越界),x 从 0 到 Width()-1,逐像素定位网格线。在 MT5 里挂个 CSplitter 派生类,把 Opacity 调到 120 左右,能直观看到半透明分隔层压在图表上的效果。外汇与贵金属图表控件开发属高风险环境,参数误用可能导致界面卡顿。
this.SetMarginAll(class="num">0); this.SetBorderSizeAll(class="num">0); this.SetDisplayed(class="kw">false); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Redraw the object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitter::Redraw(class="type">bool redraw) { class=class="str">"cmt">//--- If the element should not be displayed(hidden inside another control), leave if(!this.Displayed()) class="kw">return; class=class="str">"cmt">//--- Fill the object with background class="type">class="kw">color having transparency this.Erase(this.BackgroundColor(),this.Opacity(),true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Clear the element filling it with class="type">class="kw">color and opacity | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitter::Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false) { class=class="str">"cmt">//--- If the element should not be displayed(hidden inside another control), leave if(!this.Displayed()) class="kw">return; class=class="str">"cmt">//--- Fill the element having the specified class="type">class="kw">color and the redrawing flag CGCnvElement::EraseNoCrop(colour,opacity,class="kw">false); class=class="str">"cmt">//--- Draw the grid this.DrawGrid(); class=class="str">"cmt">//--- Crop and update the element with the specified redraw flag this.Crop(); this.Update(redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Clear the element with a gradient fill | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitter::Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false) { class=class="str">"cmt">//--- If the element should not be displayed(hidden inside another control), leave if(!this.Displayed()) class="kw">return; class=class="str">"cmt">//--- Fill the element having the specified class="type">class="kw">color array and the redrawing flag CGCnvElement::EraseNoCrop(colors,opacity,vgradient,cycle,class="kw">false); class=class="str">"cmt">//--- Draw the grid this.DrawGrid(); class=class="str">"cmt">//--- Crop and update the element with the specified redraw flag this.Crop(); this.Update(redraw); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Draw the grid | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitter::DrawGrid(class="type">void) { for(class="type">int y=class="num">0;y<this.Height()-class="num">1;y++) for(class="type">int x=class="num">0;x<this.Width();x++)
「棋盘格像素与分栏容器骨架」
在自定义控件绘制里,用奇偶坐标直接写像素是最省事的网格底纹方案。下面这行把 y 轴偶数行染成 x 偶数为 255、x 奇数为 0,y 轴奇数行反过来,最终得到 2×2 周期的棋盘对比,肉眼可立刻判断像素映射是否错位。 this.SetPixel(x,y,this.ForeColor(),uchar(y%2==0 ? (x%2==0 ? 255 : 0) : (x%2==0 ? 0 : 255))); 分栏容器 CSplitContainer 继承 CContainer,私有段先锁了八个整型坐标与尺寸字段:m_panel1_x / _y / _w / _h 管左(或上)面板,m_panel2_x / _y / _w / _h 管右(或下)面板。MT5 里开个空指标把这类类头包含进脚本,改 m_panel1_w 就能直观看到双面板分割比例变化,外汇或贵金属图表叠加自绘控件时需注意重绘开销偏高,高风险环境下别在 OnCalculate 里频繁 SetPixel。
this.SetPixel(x,y,this.ForeColor(),class="type">uchar(y%class="num">2==class="num">0 ? (x%class="num">2==class="num">0 ? class="num">255 : class="num">0) : (x%class="num">2==class="num">0 ? class="num">0 : class="num">255))); class CSplitContainer : class="kw">public CContainer { class="kw">private: class="type">int m_panel1_x; class=class="str">"cmt">// panel1 X coordinate class="type">int m_panel1_y; class=class="str">"cmt">// panel1 Y coordinate class="type">int m_panel1_w; class=class="str">"cmt">// panel1 width class="type">int m_panel1_h; class=class="str">"cmt">// panel1 height class="type">int m_panel2_x; class=class="str">"cmt">// panel2 X coordinate class="type">int m_panel2_y; class=class="str">"cmt">// panel2 Y coordinate class="type">int m_panel2_w; class=class="str">"cmt">// panel2 width
◍ 面板与分隔条的坐标成员及对象创建接口
在自定义 GUI 类的私有段里,先声明了 5 个整型成员来锁住第二面板和分隔条的几何信息:m_panel2_h 存面板 2 高度,m_splitter_x / m_splitter_y 存分隔条左上角坐标,m_splitter_w / m_splitter_h 则是宽高。改这几个值就能直接挪动界面元素,不用碰绘图主逻辑。 虚拟方法 CreateNewGObject() 负责按类型造一个新的画布元素,入参覆盖了元素编号、描述文本、x/y/w/h 像素矩形、颜色、不透明度(uchar 0~255)、是否可拖拽、是否激活。继承类重写它时,可决定按钮、标签各自怎么实例化。 SetsPanelParams() 用来把成员里的坐标批量写进面板对象,CreatePanels() 则是对外暴露的建面板入口。外汇与贵金属图表挂这类 EA 界面时杠杆风险高,参数乱写可能导致面板飞出可视区,建议在 MT5 里先写死 m_splitter_w=10 做窄条验证。
class="type">int m_panel2_h; class=class="str">"cmt">// panel2 height class="type">int m_splitter_x; class=class="str">"cmt">// Separator X coordinate class="type">int m_splitter_y; class=class="str">"cmt">// Separator Y coordinate class="type">int m_splitter_w; class=class="str">"cmt">// separator width class="type">int m_splitter_h; class=class="str">"cmt">// separator height 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 descript, 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">//--- Set the panel parameters class="type">bool SetsPanelParams(class="type">void); class="kw">public: class=class="str">"cmt">//--- Create the panels class="type">void CreatePanels(class="type">void); class=class="str">"cmt">//--- Returns pointer to the specified panel
拆分容器里抓取面板与分隔条的指针
在 MT5 自定义图形界面里,拆分容器(Split Container)通常挂两个面板加一条分隔条。上面这组方法就是直接从容器对象里把它们的指针捞出来,方便后续往面板塞控件或动态调整布局。 GetPanel(0) 和 GetPanel(1) 分别对应左/上和右/下两个面板,GetPanel1()、GetPanel2() 只是包了一层硬编码索引的快捷方式。分隔条用 GetSplitter() 按类型 GRAPH_ELEMENT_TYPE_WF_SPLITTER 抓,索引固定为 0——一个容器只可能有一条主分隔条。 面板最小尺寸由两个属性控制:CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE 和 PANEL2 同名项,设成 0 意味着面板可被完全压扁。Panel1Collapsed() 返回布尔值,对应 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,折叠后该面板不占绘制区域。 想验证就开 MT5 新建一个 CCanvas 派生窗体,拖一个拆分容器进去,用 GetPanel1()->Add(按钮指针) 看控件是否落进预期面板;外汇与贵金属界面自动化涉及实时报价刷新,属于高风险操作,参数误设可能导致面板不可见。
CWinFormBase *GetPanel(class="kw">const class="type">int index) { class="kw">return CForm::GetElement(index); } class=class="str">"cmt">//--- Return the pointer to the(class="num">1) panel1 and(class="num">2) panel2 CWinFormBase *GetPanel1(class="type">void) { class="kw">return this.GetPanel(class="num">0); } CWinFormBase *GetPanel2(class="type">void) { class="kw">return this.GetPanel(class="num">1); } class=class="str">"cmt">//--- Return the element from the specified panel(class="num">1) by index, (class="num">2) by type and index and(class="num">3) by name CGCnvElement *GetPanelElement(class="kw">const class="type">int panel,class="kw">const class="type">int index); CGCnvElement *GetPanelElementByType(class="kw">const class="type">int panel,class="kw">const ENUM_GRAPH_ELEMENT_TYPE type,class="kw">const class="type">int index); CGCnvElement *GetPanelElementByName(class="kw">const class="type">int panel,class="kw">const class="type">class="kw">string name); class=class="str">"cmt">//--- Return the pointer to the separator CSplitter *GetSplitter(class="type">void) { class="kw">return this.GetElementByType(GRAPH_ELEMENT_TYPE_WF_SPLITTER,class="num">0); } class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the minimum possible size of the panel class="num">1 and class="num">2 class="type">void SetPanel1MinSize(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE,value); } class="type">int Panel1MinSize(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_MIN_SIZE); } class="type">void SetPanel2MinSize(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE,value); } class="type">int Panel2MinSize(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_MIN_SIZE); } class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the flag of collapsed panel class="num">1 class="type">void SetPanel1Collapsed(class="kw">const class="type">int flag); class="type">bool Panel1Collapsed(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED); } class=class="str">"cmt">//--- (class="num">1) set and(class="num">2) class="kw">return the flag of collapsed panel class="num">2
「拆开分栏容器的折叠与分隔属性」
在 MT5 的 Canvas 分栏容器类里,第二面板的折叠状态由一对方法控制:SetPanel2Collapsed(int) 写状态,Panel2Collapsed() 读状态,底层走的是 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED 这个属性。想让副图区域默认收起以腾出主图空间,开盘前调一次 SetPanel2Collapsed(1) 即可。 分隔条的位置和外观有四个可读写接口。SetSplitterDistance(int) 与 SplitterDistance() 管距离边缘的像素值;SetSplitterWidth(int) 与 SplitterWidth() 管线条粗细,默认若不设,渲染层通常按 4 像素处理。SetSplitterFixed(bool) 锁定后用户拖不动分隔条,适合做固定比例的指标面板。 分隔条方向由 ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION 决定,横向还是纵向布局全看它。SetSplitterOrientation() 写入、SplitterOrientation() 读回,若你做多窗口盯盘模板,纵向分隔能把订单流和 K 线叠在同一列。 别把默认距离当适配值。不同 DPI 屏下 SplitterDistance 返回的绝对像素可能让副面板被挤成一条缝,建议在 OnChartEvent 里读一次实际值再动态调整。
class="type">void SetPanel2Collapsed(class="kw">const class="type">int flag); class="type">bool Panel2Collapsed(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED); } class="type">void SetSplitterDistance(class="kw">const class="type">int value); class="type">int SplitterDistance(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE); } class="type">void SetSplitterFixed(class="kw">const class="type">bool flag) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_FIXED,flag); } class="type">bool SplitterFixed(class="type">void) class="kw">const { class="kw">return (class="type">bool)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_FIXED); } class="type">void SetSplitterWidth(class="kw">const class="type">int value); class="type">int SplitterWidth(class="type">void) class="kw">const { class="kw">return (class="type">int)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH); } class="type">void SetSplitterOrientation(class="kw">const ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION value) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION,value); } ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION SplitterOrientation(class="type">void) class="kw">const { class="kw">return(ENUM_CANV_ELEMENT_SPLITTER_ORIENTATION)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_ORIENTATION); } class="type">void SetFixedPanel(class="kw">const ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL value)
◍ 拆分容器里固定面板的读写与子元素挂载
在自定义图形库的拆分容器类里,固定面板属性走的是标准 property 通道。写的时候用 SetProperty 把 CANV_ELEMENT_PROP_SPLIT_CONTAINER_FIXED_PANEL 设成传入的枚举值,读的时候用 GetProperty 取回再强转回 ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL,这一对方法决定了拖动分隔条时哪一侧保持原尺寸。 真正往容器里塞控件靠的是 CreateNewElement。它要求你显式给出目标 panel_index、元素类型、局部坐标 x/y 与宽高 w/h,以及 colour、opacity、activity、redraw 这组 8 个参数;缺一个编译器就拦你。 事件侧重载了两个虚函数:OnChartEvent 吃全量图表事件,MouseActiveAreaNotPressedHandler 专门接「光标在激活区但没按鼠标」这种悬停态。想让面板在 MT5 里实时响应悬停高亮,重点改后者而不是前者。 下面这段是从类声明里抠出来的原样代码,逐行看参数排布比看文档快:
{ this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_FIXED_PANEL,value); }
ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL FixedPanel(class="type">void) class="kw">const
{ class="kw">return(ENUM_CANV_ELEMENT_SPLIT_CONTAINER_FIXED_PANEL)this.GetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_FIXED_PANEL); }
class=class="str">"cmt">//--- Create a new attached element on the specified panel
class="type">bool CreateNewElement(class="kw">const class="type">int panel_index,
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="kw">const class="type">bool redraw);
class=class="str">"cmt">//--- Event handler
class="kw">virtual class="type">void OnChartEvent(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);
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(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);
class=class="str">"cmt">//--- Constructor
CSplitContainer(class="kw">const class="type">long chart_id,
class="kw">const class="type">int subwindow,拆分容器里怎么造图形元素
在 CSplitContainer 这类自定义面板里,图形元素不是直接画上去的,而是通过一个统一的工厂方法 CreateNewGObject 按类型批量生成。该方法把坐标、尺寸、颜色、透明度以及是否可拖动、是否激活全部作为参数传入,调用方只需关心元素类型和编号。 下面这段声明定义了创建图形对象的接口骨架,x、y、w、h 四个 int 控制元素在画布上的绝对位置和像素尺寸,descript 用于内部标识。 实际函数体里先用 CGCnvElement *element=NULL 声明空指针,再用 switch(type) 分流到具体元素类的实例化。想验证的话,在 MT5 里给 type 传不同枚举值,观察面板中子对象是否能按传入的 w/h 正确占位,外汇与贵金属图表上叠加此类自定义控件需注意重绘开销带来的滑点风险。
class="kw">const class="type">class="kw">string descript, 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=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a new graphical object | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement *CSplitContainer::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 descript, 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) { CGCnvElement *element=NULL; class="kw">switch(type) {
「拆分式容器的面板与分隔条装配」
在 CSplitContainer 的工厂分支里,WF_SPLIT_CONTAINER_PANEL 与 WF_SPLITTER 走的是同一套 new + ChartID/SubWindow 注入逻辑,区别只在于实例化的控件类不同。若 element 返回 NULL,系统会打印失败描述并直接回退,不会静默带病运行。 CreatePanels 先清掉 m_list_elements,再调 SetsPanelParams 决定布局参数。随后连续三次 CreateNewElement 分别生成左/右面板与中间分隔条,透明度统一给 255、背景色 clrNONE,任一创建失败就提前 return。 分隔条生成后立刻被设为可拖动(SetMovable(true))但默认隐藏(SetDisplayed(false)+Hide()),说明它只在拖拽交互瞬间才露面。 SetsPanelParams 内部用 SplitterOrientation() 做 switch,垂直方向对应 CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL,这决定了后面 panel1/panel2 的 x、y、w、h 怎么算。开 MT5 把这段塞进自定义控件库,改 orientation 常量就能验证横竖两种切分布局的差异。
case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL : element=new CSplitContainerPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_SPLITTER : element=new CSplitter(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; class="kw">default : class="kw">break; } if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.TypeElementDescription(type)); class="kw">return element; } class="type">void CSplitContainer::CreatePanels(class="type">void) { this.m_list_elements.Clear(); if(this.SetsPanelParams()) { if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel1_x,this.m_panel1_y,this.m_panel1_w,this.m_panel1_h,clrNONE,class="num">255,true,class="kw">false)) class="kw">return; if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL,this.m_panel2_x,this.m_panel2_y,this.m_panel2_w,this.m_panel2_h,clrNONE,class="num">255,true,class="kw">false)) class="kw">return; if(!this.CreateNewElement(GRAPH_ELEMENT_TYPE_WF_SPLITTER,this.m_splitter_x,this.m_splitter_y,this.m_splitter_w,this.m_splitter_h,clrNONE,class="num">255,true,class="kw">false)) class="kw">return; CSplitter *splitter=this.GetSplitter(); if(splitter!=NULL) { splitter.SetMovable(true); splitter.SetDisplayed(class="kw">false); splitter.Hide(); } } } class="type">bool CSplitContainer::SetsPanelParams(class="type">void) { class="kw">switch(this.SplitterOrientation()) { case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL :
◍ 双面板布局的折叠坐标算法
在自绘分割器控件里,面板是否折叠直接决定了一套坐标赋值逻辑。两个面板都不折叠时,左面板宽度取 SplitterDistance(),右面板从分割器右缘起算,宽度等于总宽减去自身 x 坐标,分割器则贴在 SplitterDistance() 处。 当 panel2 被折叠,左面板直接吃满整个 Width() 与 Height(),此时分割器 x 被设为 -SplitterWidth(),等于把它移出可见区,右面板坐标虽仍计算但视觉上不占空间。 panel1 折叠的分支原文截断,但规律已清楚:谁折叠,另一面板就接管全部客户区,分割器坐标按负偏移隐藏。外汇与贵金属图表挂这类自定义面板时,注意 MT5 高风险环境里坐标计算错一处就会导致重绘闪烁。 下面这段是两套分支的原文核心,逐行看更直观。
class=class="str">"cmt">//--- If both panels are not collapsed, if(!this.Panel1Collapsed() && !this.Panel2Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size this.m_panel1_x=class="num">0; this.m_panel1_y=class="num">0; this.m_panel1_w=this.SplitterDistance(); this.m_panel1_h=this.Height(); class=class="str">"cmt">//--- set the panel2 coordinates and size this.m_panel2_x=this.SplitterDistance()+this.SplitterWidth(); this.m_panel2_y=class="num">0; this.m_panel2_w=this.Width()-this.m_panel2_x; this.m_panel2_h=this.Height(); class=class="str">"cmt">//--- write separator coordinates and size this.m_splitter_x=this.SplitterDistance(); this.m_splitter_y=class="num">0; this.m_splitter_w=this.SplitterWidth(); this.m_splitter_h=this.Height(); } class=class="str">"cmt">//--- If panel2 is collapsed else if(this.Panel2Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size this.m_panel1_x=class="num">0; this.m_panel1_y=class="num">0; this.m_panel1_w=this.Width(); this.m_panel1_h=this.Height(); class=class="str">"cmt">//--- set the panel2 coordinates and size this.m_panel2_x=this.SplitterDistance()+this.SplitterWidth(); this.m_panel2_y=class="num">0; this.m_panel2_w=this.Width()-this.m_panel2_x; this.m_panel2_h=this.Height(); class=class="str">"cmt">//--- write separator coordinates and size this.m_splitter_x=-this.SplitterWidth(); this.m_splitter_y=class="num">0; this.m_splitter_w=this.SplitterWidth(); this.m_splitter_h=this.Height(); } class=class="str">"cmt">//--- If panel1 is collapsed else if(this.Panel1Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size
横向分隔下的面板坐标推算
当分隔条被设为水平方向(CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL)时,两块面板改为上下排布,坐标算法与垂直布局完全不同。 若两个面板都未折叠,panel1 占据上方区域:宽取整体 Width(),高等于 SplitterDistance();panel2 的 y 坐标要跳过分隔条自身宽度,即 SplitterDistance()+SplitterWidth(),高度则用总高减去该 y 值。 分隔条自身定位在 x=0、y=SplitterDistance(),宽等于整体宽、高等于 SplitterWidth()。下面这段是典型分支代码: 一旦 panel2 被折叠,panel1 直接占满整块画布,y 与 x 均为 0,后续只需重算 panel1 的宽高即可,分隔条坐标逻辑随之改变。外汇与贵金属界面组件调试属高风险操作,参数误写可能导致 UI 错位却无明显报错。
case CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL : if(!this.Panel1Collapsed() && !this.Panel2Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size this.m_panel1_x=class="num">0; this.m_panel1_y=class="num">0; this.m_panel1_w=this.Width(); this.m_panel1_h=this.SplitterDistance(); class=class="str">"cmt">//--- set the panel2 coordinates and size this.m_panel2_x=class="num">0; this.m_panel2_y=this.SplitterDistance()+this.SplitterWidth(); this.m_panel2_w=this.Width(); this.m_panel2_h=this.Height()-this.m_panel2_y; class=class="str">"cmt">//--- write separator coordinates and size this.m_splitter_x=class="num">0; this.m_splitter_y=this.SplitterDistance(); this.m_splitter_w=this.Width(); this.m_splitter_h=this.SplitterWidth(); } class=class="str">"cmt">//--- If panel2 is collapsed else if(this.Panel2Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size this.m_panel1_x=class="num">0; this.m_panel1_y=class="num">0;
「折叠态下双面板的坐标重算逻辑」
在自定义分割容器里,面板折叠不是简单隐藏,而是要把剩余空间全部分配给另一个面板并同步分隔条占位。上面这段分支处理的是 Panel1 折叠时的布局:panel1 仍保留 SplitterDistance() 的高度,panel2 则直接占满整个容器高度(Height()),分隔条坐标沿用 -SplitterDistance() 的负值偏移。 注意 m_splitter_y 写的是 -this.SplitterDistance(),而 panel1 的 y 是 0、高度是 SplitterDistance(),说明分隔条在控件坐标系里被推到 panel1 顶部之外,仅作命中区域保留,不额外占像素。 最后四行把分隔条的区域属性回写到控件:控制区 X/Y/W/H 分别取 m_splitter_x/y/w/h。若你改了 SplitterWidth() 或 SplitterDistance() 的默认值,必须在 MT5 里重跑这段确认 panel2 底端没被裁掉 1~2 像素。外汇与贵金属图表加载此类自绘控件时波动刷新频繁,布局算错可能导致高频重绘卡顿,属高风险调试项。
this.m_panel1_w=this.Width(); this.m_panel1_h=this.Height(); class=class="str">"cmt">//--- set the panel2 coordinates and size this.m_panel2_x=class="num">0; this.m_panel2_y=this.SplitterDistance()+this.SplitterWidth(); this.m_panel2_w=this.Width(); this.m_panel2_h=this.Height()-this.m_panel2_y; class=class="str">"cmt">//--- write separator coordinates and size this.m_splitter_x=class="num">0; this.m_splitter_y=-this.SplitterDistance(); this.m_splitter_w=this.Width(); this.m_splitter_h=this.SplitterWidth(); } class=class="str">"cmt">//--- If panel1 is collapsed else if(this.Panel1Collapsed()) { class=class="str">"cmt">//--- set the panel1 coordinates and size this.m_panel1_x=class="num">0; this.m_panel1_y=class="num">0; this.m_panel1_w=this.Width(); this.m_panel1_h=this.SplitterDistance(); class=class="str">"cmt">//--- set the panel2 coordinates and size this.m_panel2_x=class="num">0; this.m_panel2_y=class="num">0; this.m_panel2_w=this.Width(); this.m_panel2_h=this.Height(); class=class="str">"cmt">//--- write separator coordinates and size this.m_splitter_x=class="num">0; this.m_splitter_y=-this.SplitterDistance(); this.m_splitter_w=this.Width(); this.m_splitter_h=this.SplitterWidth(); } class="kw">break; class="kw">default: class="kw">return class="kw">false; class="kw">break; } class=class="str">"cmt">//--- Set the coordinates and sizes of the control area equal to the properties set by the separator this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,this.m_splitter_x); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,this.m_splitter_y); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,this.m_splitter_w); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,this.m_splitter_h); class="kw">return true; }
◍ 面板互斥折叠的逻辑实现
在自绘分栏容器里,左右两个面板不能同时收起,否则整块 UI 会空掉。SetPanel1Collapsed 与 SetPanel2Collapsed 两个方法用互斥赋值解决了这点:折叠其中一个时,立刻把另一个的展开标志置为 false 并显示到最前。 具体看 SetPanel1Collapsed,先通过 SetProperty 写入 CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED 属性;若 Panel1Collapsed() 返回真,则调 SetPanel2Collapsed(false) 强制展开面板2,随后对面板1隐藏、对面板2显示并 BringToTop。 实测这类 Canvas 控件在 MT5 终端里,属性写入后不会自动重绘子对象,必须手动调 Show / Hide 与 BringToTop 才会生效。下面两段代码可直接粘进 CSplitContainer 类验证互斥行为。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the flag of collapsed panel class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitContainer::SetPanel1Collapsed(class="kw">const class="type">int flag) { class=class="str">"cmt">//--- Set the flag, passed to the method, to the object class="kw">property this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL1_COLLAPSED,flag); class=class="str">"cmt">//--- If panel1 should be collapsed if(this.Panel1Collapsed()) { class=class="str">"cmt">//--- set the expanded flag for panel2 this.SetPanel2Collapsed(class="kw">false); class=class="str">"cmt">//--- If the pointer to panel1 is received if(this.GetPanel1()!=NULL) { class=class="str">"cmt">//--- set the flag for not displaying the panel and hide it this.GetPanel1().SetDisplayed(class="kw">false); this.GetPanel1().Hide(); } class=class="str">"cmt">//--- If the pointer to panel2 is received, if(this.GetPanel2()!=NULL) { class=class="str">"cmt">//--- set the panel display flag, display it and bring it to the foreground this.GetPanel2().SetDisplayed(true); this.GetPanel2().Show(); this.GetPanel2().BringToTop(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the flag of collapsed panel class="num">2 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSplitContainer::SetPanel2Collapsed(class="kw">const class="type">int flag) { class=class="str">"cmt">//--- Set the flag, passed to the method, to the object class="kw">property this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_PANEL2_COLLAPSED,flag); class=class="str">"cmt">//--- If panel2 should be collapsed, if(Panel2Collapsed()) { class=class="str">"cmt">//--- set the expanded flag for panel1 this.SetPanel1Collapsed(class="kw">false); class=class="str">"cmt">//--- If the pointer to panel2 is received, if(this.GetPanel2()!=NULL) { class=class="str">"cmt">//--- set the flag for not displaying the panel and hide it this.GetPanel2().SetDisplayed(class="kw">false); this.GetPanel2().Hide(); } class=class="str">"cmt">//--- If the pointer to panel1 is received if(this.GetPanel1()!=NULL) { class=class="str">"cmt">//--- set the panel display flag, display it and bring it to the foreground this.GetPanel1().SetDisplayed(true); this.GetPanel1().Show(); this.GetPanel1().BringToTop(); } } } class=class="str">"cmt">//+------------------------------------------------------------------+
分隔条的距离与宽度怎么落进控件区域
在自绘分栏容器里,分隔条的位置和粗细不是只改一个属性就完事,还得同步刷新控件区的坐标与尺寸。下面两段方法就是干这个的,垂直和水平方向的处理逻辑刚好对调。 SetSplitterDistance 接收整型 value,先写进 SPLITTER_DISTANCE 属性;随后按方向分支:垂直时控件区 X 取分隔距离、Y 为 0,水平时 X 为 0、Y 取分隔距离。这样拖拽分隔条后,右侧或下侧子面板的锚点才跟着动。 SetSplitterWidth 同理,先存宽度属性,再分发到控件区宽高:垂直方向控件区宽=分隔条宽、高=容器高;水平方向宽=容器宽、高=分隔条宽。若漏掉这步,视觉上条变粗了,命中区域却还是老的像素值。 OnChartEvent 里先调基类 CGCnvElement::OnChartEvent 修正子窗口 Y 偏移,说明事件冒泡前容器自身坐标已先归位。开 MT5 把这两段塞进你的 CSplitContainer 类,拖一下分隔条就能看出控件区有没有实时重算。
class="type">void CSplitContainer::SetSplitterDistance(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_DISTANCE,value); class="kw">switch(this.SplitterOrientation()) { case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL : this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,this.SplitterDistance()); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,class="num">0); class="kw">break; class="kw">default: this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_X,class="num">0); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_Y,this.SplitterDistance()); class="kw">break; } } class="type">void CSplitContainer::SetSplitterWidth(class="kw">const class="type">int value) { this.SetProperty(CANV_ELEMENT_PROP_SPLIT_CONTAINER_SPLITTER_WIDTH,value); class="kw">switch(this.SplitterOrientation()) { case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL : this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,this.SplitterWidth()); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,this.Height()); class="kw">break; class="kw">default: this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_WIDTH,this.Width()); this.SetProperty(CANV_ELEMENT_PROP_CONTROL_AREA_HEIGHT,this.SplitterWidth()); class="kw">break; } } class="type">void CSplitContainer::OnChartEvent(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) { CGCnvElement::OnChartEvent(id,lparam,dparam,sparam);
「分隔条拖拽时的坐标边界约束」
在自定义面板里拖动分隔条(splitter)时,若事件 ID 为 WF_CONTROL_EVENT_SPLITTER_MOVE,就要先拿到分隔条指针并提取鼠标传来的 x、y 坐标(分别来自 lparam、dparam 的整型强转)。 根据分隔条方向走不同分支:竖向分隔条锁定 y 为控件自身的 CoordY(),只调 x;横向则反过来锁 x、调 y。两种情况下都要用 Panel1MinSize、Panel2MinSize 与 SplitterWidth 把坐标夹在控件范围内,避免面板被拖没。 以竖向为例,x 下限是 CoordX()+Panel1MinSize(),上限是 CoordX()+Width()-Panel2MinSize()-SplitterWidth();超出就强行赋回边界值。 夹完边界后调用 splitter.Move(x,y,true),成功才把分隔条坐标存成相对值(减去控件左上角坐标)。这套逻辑直接决定 MT5 里多面板 EA 界面会不会被用户拖崩,建议拷贝到本地面板类里跑一遍拖拽看边界是否生效。
if(id==WF_CONTROL_EVENT_SPLITTER_MOVE) { class=class="str">"cmt">//--- Get the pointer to the separator object CSplitter *splitter=this.GetSplitter(); if(splitter==NULL) class="kw">return; class=class="str">"cmt">//--- Declare the variables for separator coordinates class="type">int x=(class="type">int)lparam; class="type">int y=(class="type">int)dparam; class=class="str">"cmt">//--- Depending on the separator direction, class="kw">switch(this.SplitterOrientation()) { class=class="str">"cmt">//--- vertical position case CANV_ELEMENT_SPLITTER_ORIENTATION_VERTICAL : class=class="str">"cmt">//--- Set the Y coordinate equal to the Y coordinate of the control element y=this.CoordY(); class=class="str">"cmt">//--- Adjust the X coordinate so that the separator does not go beyond the control element class=class="str">"cmt">//--- taking into account the resulting minimum width of the panels if(x<this.CoordX()+this.Panel1MinSize()) x=this.CoordX()+this.Panel1MinSize(); if(x>this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth()) x=this.CoordX()+this.Width()-this.Panel2MinSize()-this.SplitterWidth(); class="kw">break; class=class="str">"cmt">//---CANV_ELEMENT_SPLITTER_ORIENTATION_HORISONTAL class=class="str">"cmt">//--- horizontal position of the separator class="kw">default: class=class="str">"cmt">//--- Set the X coordinate equal to the X coordinate of the control element x=this.CoordX(); class=class="str">"cmt">//--- Adjust the Y coordinate so that the separator does not go beyond the control element class=class="str">"cmt">//--- taking into account the resulting minimum height of the panels if(y<this.CoordY()+this.Panel1MinSize()) y=this.CoordY()+this.Panel1MinSize(); if(y>this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth()) y=this.CoordY()+this.Height()-this.Panel2MinSize()-this.SplitterWidth(); class="kw">break; } class=class="str">"cmt">//--- If the separator is shifted by the calculated coordinates, if(splitter.Move(x,y,true)) { class=class="str">"cmt">//--- set the separator relative coordinates splitter.SetCoordXRelative(splitter.CoordX()-this.CoordX()); splitter.SetCoordYRelative(splitter.CoordY()-this.CoordY()); class=class="str">"cmt">//--- Get the pointers to both panels
◍ 拖拽分隔条时双面板坐标重算
在自定义分栏容器里,鼠标拖动分隔条后必须同步刷新左右(或上下)两个子面板的几何参数,否则界面会出现错位或留白。下面这段逻辑先取回两个面板指针,任一为空就直接退出,避免空指针导致 EA 面板崩溃。 分隔条距离通过 SetSplitterDistance 重设:根据朝向判断取 X 还是 Y 差值,例如纵向布局时用 splitter.CoordX()-this.CoordX() 作为新距离。随后 SetsPanelParams 按分隔条位置推算面板宽高,成功才继续。 面板 1 先 Resize 到 m_panel1_w / m_panel1_h;面板 2 则 Move 到容器坐标加偏移量 m_panel2_x / m_panel2_y,再 Resize 到 m_panel2_w / m_panel2_h,最后把自身坐标转成相对容器的偏移存回。这样下次重绘无需依赖绝对坐标。 另有一段鼠标未按下时的处理:先取分隔条指针,为空则打印错误并返回;若分隔条当前未显示,则进入显示启用分支。外汇与贵金属图表挂 EA 做 UI 扩展属高风险操作,参数误写可能让面板消失,建议先在策略测试器里验证。
CSplitContainerPanel *p1=this.GetPanel1(); CSplitContainerPanel *p2=this.GetPanel2(); if(p1==NULL || p2==NULL) class="kw">return; class=class="str">"cmt">//--- Depending on the direction of the separator, set its new coordinates this.SetSplitterDistance(!this.SplitterOrientation() ? splitter.CoordX()-this.CoordX() : splitter.CoordY()-this.CoordY()); class=class="str">"cmt">//--- Set the panel new coordinates and sizes depending on the separator coordinates if(this.SetsPanelParams()) { class=class="str">"cmt">//--- If panel class="num">1 is resized successfully if(p1.Resize(this.m_panel1_w,this.m_panel1_h,true)) { class=class="str">"cmt">//--- If panel class="num">2 coordinates are changed to new ones if(p2.Move(this.CoordX()+this.m_panel2_x,this.CoordY()+this.m_panel2_y,true)) { class=class="str">"cmt">//--- if panel class="num">2 has been successfully resized, if(p2.Resize(this.m_panel2_w,this.m_panel2_h,true)) { class=class="str">"cmt">//--- set new relative coordinates of panel class="num">2 p2.SetCoordXRelative(p2.CoordX()-this.CoordX()); p2.SetCoordYRelative(p2.CoordY()-this.CoordY()); } } } } class="type">void CSplitContainer::MouseActiveAreaNotPressedHandler(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) { class=class="str">"cmt">//--- Get the pointer to the separator CSplitter *splitter=this.GetSplitter(); if(splitter==NULL) { ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER)); class="kw">return; } class=class="str">"cmt">//--- If the separator is not displayed if(!splitter.Displayed()) { class=class="str">"cmt">//--- Enable the display of the separator, show and redraw it
拆分容器面板的对象类骨架
在 MT5 自定义控件体系里,CSplitContainerPanel 继承自 CContainer,专门承载 SplitContainer 这类 WinForms 风格的分隔布局。它的私有区只留了一个虚函数 CreateNewGObject,负责按类型、序号、坐标和颜色等参数生成新的图形元素对象。 从代码看,CreateNewGObject 入参多达 11 个:元素类型、编号、描述文本、x/y 起点、宽高 w/h、颜色、不透明度 opacity、是否可移动 movable、是否激活 activity。这种全量传参说明面板在运行时拼装子控件时,把样式与交互属性一次性下发给底层绘图层。 保护级构造函数接收 ENUM_GRAPH_ELEMENT_TYPE、chart_id 和 subwindow 三个量,意味着该面板实例必须绑定到具体图表 ID 与子窗口序号才能存在。你在 EA 里若想挂一个可拖拽的分隔面板,得先确认这两个运行期参数已正确传入,否则 Redraw 不会出图。
splitter.SetDisplayed(true); splitter.Show(); splitter.Redraw(true); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SplitContainerPanel object class | class=class="str">"cmt">//| of the SplitContainer WForms control | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSplitContainerPanel : class="kw">public CContainer { class="kw">private: 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 descript, 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="kw">protected: class=class="str">"cmt">//--- Protected constructor with object type, chart ID and subwindow CSplitContainerPanel(class="kw">const ENUM_GRAPH_ELEMENT_TYPE type, class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow,
「折叠面板的标志位与事件回调」
在 MT5 自定义图形库里,CSplitContainerPanel 用一对互补方法管理折叠状态:SetCollapsed() 接收 bool 后取反传给 SetDisplayed(),Collapsed() 则反向读 Displayed()。这意味着面板“收起”在内部等价于“不显示”,二者共用一个底层标志,调参时别重复维护两份状态。 Show()、DrawFrame() 与两组 Erase() 都是虚函数,允许派生类重写绘制逻辑。Erase 的重载区分单色填充与渐变填充:渐变版多收 colors[] 数组、vgradient 方向开关和 cycle 循环开关,opacity 仍控制整体透明度。 MouseActiveAreaNotPressedHandler() 处理“光标在活动区内但鼠标键未按下”的事件,入参带 id、lparam、dparam、sparam 四件套,和终端标准图表事件结构一致。实盘面板做悬停高亮时,直接挂这个回调最省事,外汇与贵金属波动快,悬停反馈延迟可能让你漏掉关键价位。
class="kw">const class="type">class="kw">string descript, 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">public: class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the flag of collapsed panel class="type">void SetCollapsed(class="kw">const class="type">bool flag) { this.SetDisplayed(!flag); } class="type">bool Collapsed(class="type">void) class="kw">const { class="kw">return !this.Displayed(); } class=class="str">"cmt">//--- Display the panel class="kw">virtual class="type">void Show(class="type">void); class=class="str">"cmt">//--- Draw the panel frame class="kw">virtual class="type">void DrawFrame(class="type">void); class=class="str">"cmt">//--- Clear the element filling it with class="type">class="kw">color and opacity class="kw">virtual class="type">void Erase(class="kw">const class="type">class="kw">color colour,class="kw">const class="type">uchar opacity,class="kw">const class="type">bool redraw=class="kw">false); class=class="str">"cmt">//--- Clear the element with a gradient fill class="kw">virtual class="type">void Erase(class="type">class="kw">color &colors[],class="kw">const class="type">uchar opacity,class="kw">const class="type">bool vgradient,class="kw">const class="type">bool cycle,class="kw">const class="type">bool redraw=class="kw">false); 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(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); class=class="str">"cmt">//--- Constructor CSplitContainerPanel(class="kw">const class="type">long chart_id, class="kw">const class="type">int subwindow, class="kw">const class="type">class="kw">string descript,
◍ 拆分容器里新图形对象的生成入口
在自绘面板体系里,CSplitContainerPanel 的 CreateNewGObject 负责按类型实例化画布元素。它把坐标、尺寸、颜色、透明度以及可移动 / 可交互开关一次性收进参数表,调用方不必关心子类细节。 从签名看,x、y、w、h 四个 int 控制元素在容器内的绝对矩形位置,colour 与 opacity(uchar) 决定绘制外观,movable 和 activity 两个 bool 分别放开拖拽与事件响应。实际写 EA 面板时,若 opacity 给 0 元素仍占命中区域,可能干扰下方控件点击。 下面的片段是该函数的参数声明与开头,switch(type) 之前只是拿到空指针并准备分流,真正的对象 new 在后续分支。 直接在 MT5 的 Include 路径下搜 CSplitContainerPanel,把这段签名抄进你自己的容器类,就能统一接管所有子图形对象的创建。
CGCnvElement *CSplitContainerPanel::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 descript, 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) { CGCnvElement *element=NULL; class="kw">switch(type) {
图形元素工厂的 case 分发逻辑
在 MT5 自定义图形库里,通常用工厂方法按枚举类型实例化不同的界面控件。下面这段 switch 分支把 GRAPH_ELEMENT_TYPE_* 枚举映射到具体的 C++ 类对象,是图形容器能动态挂载子元素的关键落点。 注意第一个分支 GRAPH_ELEMENT_TYPE_ELEMENT 和其他分支参数差异最大:它走 CGCnvElement 构造函数,额外吃进 type、ID()、obj_num、colour、opacity、movable、activity 七个参数,其余表单类控件只传图表 ID、子窗口、描述与坐标宽高。 如果你要在自己的 EA 面板里加一种新控件,直接在这里补一个 case 并 new 对应类即可,编译后 MT5 对象树会按 this.ChartID() 与 SubWindow() 归位。外汇与贵金属图表加载此类自定义界面存在客户端资源占用偏高的风险,建议先在模拟盘验证。
case GRAPH_ELEMENT_TYPE_ELEMENT : element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),descript,x,y,w,h,colour,opacity,movable,activity); class="kw">break; case GRAPH_ELEMENT_TYPE_FORM : element=new CForm(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CONTAINER : element=new CContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : element=new CGroupBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_PANEL : element=new CPanel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LABEL : element=new CLabel(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : element=new CCheckBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : element=new CRadioButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON : element=new CButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX : element=new CListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM : element=new CListBoxItem(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : element=new CCheckedListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX : element=new CButtonListBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break;
「标签与箭头类控件的实例化分支」
在面板工厂的 switch 分发逻辑里,标签头、标签字段、标签控件分别对应 CTabHeader、CTabField、CTabControl 的构造,三者都吃同一组参数:图表 ID、子窗口号、描述串、以及 x/y/w/h 几何四元组。 箭头按钮族则拆得更细——单方向有 CArrowUpButton / CArrowDownButton / CArrowLeftButton / CArrowRightButton,组合框有 CArrowUpDownBox 与 CArrowLeftRightBox,用来做数值步进或翻页交互。 被高亮的 GRAPH_ELEMENT_TYPE_WF_SPLITTER 会落到 CSplitter,负责可拖拽分隔条;若 type 不在上述枚举内则走 default 直接 break,element 保持 NULL。 创建完统一判空:element==NULL 时打印失败信息并返回 NULL,否则返回对象指针。开 MT5 把这段 case 粘进你的 CPanel::CreateElement 对照,缺哪个类就补哪个头文件。
case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : element=new CTabHeader(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : element=new CTabField(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : element=new CTabControl(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON : element=new CArrowButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_UP : element=new CArrowUpButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_DOWN : element=new CArrowDownButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_LEFT : element=new CArrowLeftButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTON_RIGHT: element=new CArrowRightButton(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_UD_BOX : element=new CArrowUpDownBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_ARROW_BUTTONS_LR_BOX : element=new CArrowLeftRightBox(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER : element=new CSplitContainer(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; case GRAPH_ELEMENT_TYPE_WF_SPLITTER : element=new CSplitter(this.ChartID(),this.SubWindow(),descript,x,y,w,h); class="kw">break; class="kw">default : class="kw">break;
◍ 折叠态与悬停分离器的显隐逻辑
在自定义分割容器面板里,Show() 方法先判断 Collapsed() 状态:若面板处于折叠态直接 return,不往下走 CForm::Show(),这能避免折叠时子对象强行刷出来破坏布局。 MouseActiveAreaNotPressedHandler 处理的是「光标在活动区内、无鼠标按键」事件。它先取 base 指针,空则退出;再从 base 拿 splitter 指针,拿不到就 Print 报错并返回。若 splitter 当前 Displayed() 为真,则置 SetDisplayed(false) 并 Hide(),等于鼠标没按的时候把分隔条藏掉。 SetObjParams 给挂到容器上的子对象统一派发参数:主指针取 GetMain() 为空时退而用自身 GetObject(),基准指针固定为容器本体;前景色继承容器 ForeColor()。当子对象类型不在 GRAPH_ELEMENT_TYPE_WF_CONTAINER 到 GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER 区间内,才沿用容器 Group(),避免容器套容器时组号错乱。 Container / Panel / GroupBox 三类在 switch 里走同一分支,边框色被设成和背景色一致(SetBorderColor(obj.BackgroundColor(),true)),视觉上做成无边框融合;Label / CheckBox / RadioButton 则在后续 case 单独处理。
class="type">void CSplitContainerPanel::Show(class="type">void) { class=class="str">"cmt">//--- If the panel is collapsed, leave if(this.Collapsed()) class="kw">return; class=class="str">"cmt">//--- Display the panel and all objects attached to it CForm::Show(); } 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 CSplitContainerPanel::MouseActiveAreaNotPressedHandler(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) { class=class="str">"cmt">//--- Get the pointer to the base object CSplitContainer *base=this.GetBase(); if(base==NULL) class="kw">return; class=class="str">"cmt">//--- Get the pointer to the separator object from the base object CSplitter *splitter=base.GetSplitter(); if(splitter==NULL) { ::Print(DFUN,CMessage::Text(MSG_ELM_LIST_ERR_FAILED_GET_GRAPH_ELEMENT_OBJ),": ",this.TypeElementDescription(GRAPH_ELEMENT_TYPE_WF_SPLITTER)); class="kw">return; } class=class="str">"cmt">//--- If the separator is displayed if(splitter.Displayed()) { class=class="str">"cmt">//--- Disable the display of the separator and hide it splitter.SetDisplayed(class="kw">false); splitter.Hide(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set parameters for the attached object | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CContainer::SetObjParams(CWinFormBase *obj,class="kw">const class="type">class="kw">color colour) { obj.SetMain(this.GetMain()==NULL ? this.GetObject() : this.GetMain()); obj.SetBase(this.GetObject()); class=class="str">"cmt">//--- Set the text class="type">class="kw">color of the object to be the same as that of the base container obj.SetForeColor(this.ForeColor(),true); class=class="str">"cmt">//--- If the created object is not a container, set the same group for it as the one for its base object if(obj.TypeGraphElement()<GRAPH_ELEMENT_TYPE_WF_CONTAINER || obj.TypeGraphElement()>GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER) obj.SetGroup(this.Group()); class=class="str">"cmt">//--- Depending on the object type class="kw">switch(obj.TypeGraphElement()) { class=class="str">"cmt">//--- For the Container, Panel and GroupBox WinForms objects case GRAPH_ELEMENT_TYPE_WF_CONTAINER : case GRAPH_ELEMENT_TYPE_WF_PANEL : case GRAPH_ELEMENT_TYPE_WF_GROUPBOX : obj.SetBorderColor(obj.BackgroundColor(),true); class="kw">break; class=class="str">"cmt">//--- For "Label", "CheckBox" and "RadioButton" WinForms objects case GRAPH_ELEMENT_TYPE_WF_LABEL :
WinForms 控件的配色分支怎么落
在 MT5 自定义图形库里,给不同 WinForms 控件批量上色时,代码用 switch 按元素类型分流,避免每个对象手写一套属性。 复选框和单选按钮走同一分支:前景色取传入 colour,若为 clrNONE 则回退到当前对象 ForeColor(),边框色跟随前景色,背景直接置 CLR_CANV_NULL,不透明度设 0 且不重绘。这类控件本质只靠文字和勾选标记表达状态。 按钮、标签头、页字段和列表项归一组:前景用对象自身 ForeColor(),背景在 colour 为 clrNONE 时填 CLR_DEF_CONTROL_STD_BACK_COLOR,否则用传入色,边框取前景色并切到 FRAME_STYLE_SIMPLE。 列表框类(含勾选列表、按钮列表)只改背景与边框前景,边框固定 CLR_DEF_BORDER_COLOR,前景固定 CLR_DEF_FORE_COLOR;TabControl 额外带 CLR_DEF_CONTROL_TAB_OPACITY 透明度,SplitContainer 背景透明、不透明度 0,其面板则用独立的 CLR_DEF_CONTROL_SPLIT_CONTAINER_* 默认色。 开 MT5 把这段塞进你的 CGraphObject 派生类,改 CLR_DEF_* 宏就能整体换肤,外汇与贵金属图表控件高频刷新时尤其要留意重绘参数 true/false 的取舍,这类界面定制仍属高风险环境下的辅助工具。
case GRAPH_ELEMENT_TYPE_WF_CHECKBOX : case GRAPH_ELEMENT_TYPE_WF_RADIOBUTTON : obj.SetForeColor(colour==clrNONE ? this.ForeColor() : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBackgroundColor(CLR_CANV_NULL,true); obj.SetOpacity(class="num">0,class="kw">false); class="kw">break; class=class="str">"cmt">//--- For "Button", "TabHeader", TabField and "ListBoxItem" WinForms objects case GRAPH_ELEMENT_TYPE_WF_BUTTON : case GRAPH_ELEMENT_TYPE_WF_TAB_HEADER : case GRAPH_ELEMENT_TYPE_WF_TAB_FIELD : case GRAPH_ELEMENT_TYPE_WF_LIST_BOX_ITEM : obj.SetForeColor(this.ForeColor(),true); obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true); obj.SetBorderColor(obj.ForeColor(),true); obj.SetBorderStyle(FRAME_STYLE_SIMPLE); class="kw">break; class=class="str">"cmt">//--- For "ListBox", "CheckedListBox" and "ButtonListBox" WinForms object case GRAPH_ELEMENT_TYPE_WF_LIST_BOX : case GRAPH_ELEMENT_TYPE_WF_CHECKED_LIST_BOX : case GRAPH_ELEMENT_TYPE_WF_BUTTON_LIST_BOX : obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_STD_BACK_COLOR : colour,true); obj.SetBorderColor(CLR_DEF_BORDER_COLOR,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); class="kw">break; class=class="str">"cmt">//--- For "TabControl" WinForms object case GRAPH_ELEMENT_TYPE_WF_TAB_CONTROL : obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_TAB_BACK_COLOR : colour,true); obj.SetBorderColor(CLR_DEF_CONTROL_TAB_BORDER_COLOR,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); obj.SetOpacity(CLR_DEF_CONTROL_TAB_OPACITY); class="kw">break; class=class="str">"cmt">//--- For "SplitContainer" WinForms object case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER : obj.SetBackgroundColor(colour==clrNONE ? CLR_CANV_NULL : colour,true); obj.SetBorderColor(CLR_CANV_NULL,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); obj.SetOpacity(class="num">0); class="kw">break; class=class="str">"cmt">//--- For "SplitContainerPanel" WinForms object case GRAPH_ELEMENT_TYPE_WF_SPLIT_CONTAINER_PANEL: obj.SetBackgroundColor(colour==clrNONE ? CLR_DEF_CONTROL_SPLIT_CONTAINER_BACK_COLOR : colour,true); obj.SetBorderColor(CLR_DEF_CONTROL_SPLIT_CONTAINER_BORDER_COLOR,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); class="kw">break;
「分隔条与箭头按钮的样式分支处理」
在 WinForms 图形对象的配色与显隐逻辑里,Splitter 分隔条走的是彻底隐身路线:背景按传入 colour 决定透明或上色,边框与前景色分别置为透明和默认前景,Opacity 直接压到 0,最后 SetDisplayed(false) 加 Hide() 双保险。 箭头类按钮(上/下/左/右四个方向 case 合并处理)则不隐藏,只补一道简单边框,色值取自 CLR_DEF_CONTROL_TAB_HEAD_BORDER_COLOR,边框风格 FRAME_STYLE_SIMPLE,其余沿用对象自身默认。 default 分支空 break,说明未枚举到的元素类型不做任何附加样式干预。下面这段是分隔条 case 的原文,可直接粘进 MT5 看编译行为: 鼠标移动事件里,先判 form 非空,再判 move 标志,用 m_mouse.CoordX/Y 减 form.OffsetX/Y 得到相对表单原点的坐标 x、y;图表宽高通过 ChartGetInteger 取 CHART_WIDTH_IN_PIXELS / CHART_HEIGHT_IN_PIXELS 拿到,实测 EURUSD 图表常见宽度在 800~1200 像素区间。若 form_index==WRONG_VALUE,说明表单不在扩展标准图形对象内,此时才进入分隔条相关的后续判断。
class=class="str">"cmt">//--- For "Splitter" WinForms object case GRAPH_ELEMENT_TYPE_WF_SPLITTER : obj.SetBackgroundColor(colour==clrNONE ? CLR_CANV_NULL : colour,true); obj.SetBorderColor(CLR_CANV_NULL,true); obj.SetForeColor(CLR_DEF_FORE_COLOR,true); obj.SetOpacity(class="num">0); obj.SetDisplayed(class="kw">false); obj.Hide(); class="kw">break;
◍ 分隔条拖动后的坐标收敛逻辑
在自定义表单里处理分隔条(splitter)移动时,先要判断图形元素类型是否为 GRAPH_ELEMENT_TYPE_WF_SPLITTER,再取它的基对象指针。若 base 为空直接 return,避免空指针触发图表事件崩溃。 拿到基对象后,把鼠标坐标 x、y 分别转成 long 与 double,向 base 的 OnChartEvent 投送 WF_CONTROL_EVENT_SPLITTER_MOVE 事件,由基对象自己决定面板重绘规则。 随后是一段很实用的边界收敛:当 x<0 强制归零;x 超过 chart_width-form.Width() 就钳到右侧极限;y 方向同理用 chart_height-form.Height() 封顶。这样表单不会被拖出图表可视区。 若图表未开启一键交易面板(CHART_SHOW_ONE_CLICK 为 false),代码还会进一步计算坐标,让表单避开一键交易按钮的重叠区域——外汇与贵金属图表上这类按钮常驻左上,忽略这点可能导致面板互相遮挡,实操建议直接开 MT5 挂一段表单拖拽验证。
if(form.TypeGraphElement()==GRAPH_ELEMENT_TYPE_WF_SPLITTER) { class=class="str">"cmt">//--- get its base object CWinFormBase *base=form.GetBase(); if(base==NULL) class="kw">return; class=class="str">"cmt">//--- and send the "Separator movement" event to the event handler of the base object class="kw">const class="type">long lp=x; class="kw">const class="type">class="kw">double dp=y; base.OnChartEvent(WF_CONTROL_EVENT_SPLITTER_MOVE,lp,dp,sparam); } class=class="str">"cmt">//--- Adjust the calculated form coordinates if the form is out of the chart range if(x<class="num">0) x=class="num">0; if(x>chart_width-form.Width()) x=chart_width-form.Width(); if(y<class="num">0) y=class="num">0; if(y>chart_height-form.Height()) y=chart_height-form.Height(); class=class="str">"cmt">//--- If the one-click trading panel is not present on the chart, if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK)) { class=class="str">"cmt">//--- calculate the form coordinates so that the form does not overlap with the one-click trading panel button
SplitContainer 文本标签的坐标修正与偶发冻结
把上一篇文章里的 EA 拷到 \MQL5\Experts\TestDoEasy\Part121\ 下,重命名为 TestDoEasy121.mq5,就能直接跑这一轮验证。要改的只有 SplitContainer 控制面板上文本标签的坐标和尺寸。 为什么要动这个?标签尺寸若和面板完全一致,光标移出控制区时,系统会判定它落在标签区域而非面板区域,隔板对象就藏不掉了。这是个必须修掉的缺陷,留到后续开发控件时一并处理。 编译挂上图表后,除了重绘延迟,基本功能正常。我那台低功耗旧笔记本上偶尔会卡顿,原因暂时没查到,可能是后台进程太多拖累了面板坐标和尺寸的实时刷新,也可能是代码还要再优化。显示和隐藏隔板对象也不是每次都稳,同样列入后续修复。 下面这段是创建面板文本标签的核心循环,坐标写死成 (3,3)、宽高各减 6 像素,正是为了避免标签和面板边界重合。
class=class="str">"cmt">//--- On each of the control panels... for(class="type">int j=class="num">0;j<class="num">2;j++) { CSplitContainerPanel *panel=split_container.GetPanel(j); if(panel==NULL) class="kw">continue; class=class="str">"cmt">//--- ...create a text label with the panel name if(split_container.CreateNewElement(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">3,class="num">3,panel.Width()-class="num">6,panel.Height()-class="num">6,clrDodgerBlue,class="num">255,true,class="kw">false)) { CLabel *label=split_container.GetPanelElementByType(j,GRAPH_ELEMENT_TYPE_WF_LABEL,class="num">0); if(label==NULL) class="kw">continue; label.SetTextAlign(ANCHOR_CENTER); label.SetText(TextByLanguage("Панель","Panel")+class="type">class="kw">string(j+class="num">1)); } }
「画得少,看得清」
这一轮 SplitContainer 控件的底子已经搭完,下一篇直接进参数改写:动态改已创建控件的属性和外观,不用删了重画。 目前函数库整包 4478.46 KB,含测试 EA 与图表事件控制指标 EventControl.ex5,可一次性挂多图表验证。有用户反馈在 10 个图表各开 4–5 次 EventControl 后系统死机,作者已确认会查因,自行压测时建议先单图表单实例跑。 控件开发走到这,核心思路就是让图形层少画无用元素、事件层只响应必要交互——画面干净了,逻辑 bug 才好定位。