DoEasy. 控件 (第 19 部分): 在 TabControl 中滚动选项卡、WinForms 对象事件(基础篇)
◍ 给 TabControl 加上选项卡滚动能力
DoEasy 库这一轮把 WinForms 风格的 TabControl 推进到可滚动选项卡阶段:当选项卡数量超出控件可视宽度时,不再被截断或挤压,而是出现滚动逻辑来切换显示页。 原文示例发布于 2023 年 1 月 25 日 09:55,截至归档该篇获得 683 次查看、3 条留言,说明这类底层控件封装在中低频更新中仍有稳定受众。 事件层同步接入了 WinForms 对象事件,意味着选项卡切换、滚动触发都能走统一回调,省去在 EA 里手写鼠标命中检测。外汇与贵金属品种加载此类自定义控件存在平台兼容性风险,建议在 MT5 策略测试器外先用独立指标面板验证。
用事件模型驱动选项卡标题栏的滚动
上一节里验证了选中被部分遮挡的标题时,选项卡标题栏会向左平移并完整露出。这一节在已有功能上,补充从左右、上下移动标题栏的方法。上下滚动时要区分标题行在左还是在右,选中隐藏标题需完整显示、或按下上下左右滚动按钮时,都会调用这些方法。 要分辨按下了哪个按钮、哪个控件,得靠事件模型:按钮被按下会发事件,函数库截获后转交给按钮所依附元素的事件处理程序。当前主元素和基准元素的名称通过事件处理程序的 'sparam' 参数传递。主控件是承载事件的对象控件,被视为基准;但若基准是复合控件、还挂了按钮等子控件,事件实际发生在子控件上,就找不到直接依附的基准元素。 解决办法是在 'sparam' 里同时传主对象、基准对象、以及发生事件的对象名称。针对单击按钮这种特殊情况,还在 'dparam' 里发送基准对象类型。这样已知基准类型,就能列出主对象里该类型的所有控件,遍历时匹配 'sparam' 末尾传来的事件对象名,定位到具体被点的控件。 这套结构在更复杂对象下不算特别稳,但函数库现阶段够用。等做出嵌套控件的实例,就能反过来验证更通用的事件识别方式——记住从简单到复杂。目前函数库已能隐藏/显示控件:只隐主元素,挂着的子元素跟着隐;但若有控件内部设了自身可见性、不随主元素走,父元素显示时它仍应保持隐藏。为此要加一个图形元素的显示标志,主对象隐藏再显示后,清过显示标志的控件不会自动露出来,得从基准控件显式打开。 外汇与贵金属自动化界面开发高风险,MT5 上验证前先在小周期图表跑通事件链路,避免实盘误触。
「给 DoEasy 控件加上通用点击事件通道」
做滚动控件时没必要给每个按钮单独造事件 ID。直接在 Defines.mqh 的 WinForms 事件枚举里补一个通用单击事件,再顺手加一个取消单击的事件常量,后续下拉、滚动条都能复用同一套消息结构。 图形元素类 GCnvElement 里新增了一个整型属性,属性总数从 96 调到 97。这个标志位控制“非隐藏时是否真的画出来”:主控件显示了,但子控件的该标志是 false,子控件依然不显示,必须手动把 Displayed 设 true 再调 Show() 才露面。 事件消息怎么发:Form.mqh 里写了个虚方法,按钮、单选、复选框的鼠标左键事件处理中都插入了发送调用。lparam 传当前对象 ID,dparam 传基准对象类型,sparam 用分号拼了主对象、基准对象、当前对象三个名字,收到消息就能反查事件源。 ArrowUpDownBox 和 ArrowLeftRightBox 之前只能跟着 Engine.mqh 整库编,单独编会挂。把包含文件从面板对象改成只用到的类文件后,两个都能独立编译了。 TabControl 的标题超出容器时,原本会在左边缘漏出 2 像素窄条。裁剪方法里按是否选中动态把可见区域缩 2 像素,选中状态不变,未选中就减掉,窄边不再露。滚动标题栏的六个方向方法(左/右/上/下组合)已接进事件处理,点隐藏标题或箭头按钮都会触发对应滚动。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| List of possible WinForms control events | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_WF_CONTROL_EVENT { WF_CONTROL_EVENT_NO_EVENT = GRAPH_OBJ_EVENTS_NEXT_CODE,class=class="str">"cmt">// No event WF_CONTROL_EVENT_CLICK, class=class="str">"cmt">// "Click on the control" event WF_CONTROL_EVENT_CLICK_CANCEL, class=class="str">"cmt">// "Canceling the click on the control" event WF_CONTROL_EVENT_TAB_SELECT, class=class="str">"cmt">// "TabControl tab selection" event
◍ 控件事件枚举与画布元素整数属性
在 MT5 自定义控件框架里,滚动方向的交互被拆成四个独立事件常量:左、右、上、下分别触发 WF_CONTROL_EVENT_CLICK_SCROLL_LEFT 等。这意味着你想捕获鼠标滚轮或方向键对控件的滚动,必须逐个分支处理,而不是靠一个通用 scroll 事件糊弄过去。 画布上的图形元素用 ENUM_CANV_ELEMENT_PROP_INTEGER 管理整数属性,从 CANV_ELEMENT_PROP_ID=0 开始排,一直到 CANV_ELEMENT_PROP_ALIGNMENT。其中 CANV_ELEMENT_PROP_DISPLAYED 控制控件是否处于非隐藏的可视状态,CANV_ELEMENT_PROP_ZORDER 决定图表点击时谁优先吃到事件——做叠层面板时这两个值调错,鼠标就会点穿到下层。 宏定义里写死 CANV_ELEMENT_PROP_INTEGER_TOTAL 为 97,CANV_ELEMENT_PROP_INTEGER_SKIP 为 0。也就是说全部 97 个整数属性都参与排序基线,FIRST_CANV_ELEMENT_DBL_PROP 直接等于 97,后面浮点、字符串属性的偏移都基于这个数算。开 MT5 把这段枚举贴进头文件,搜一下 97 的引用位置,就能看清整套属性表的排布逻辑。
WF_CONTROL_EVENT_CLICK_SCROLL_LEFT, class=class="str">"cmt">// "Clicking the control left button" event WF_CONTROL_EVENT_CLICK_SCROLL_RIGHT, class=class="str">"cmt">// "Clicking the control right button" event WF_CONTROL_EVENT_CLICK_SCROLL_UP, class=class="str">"cmt">// "Clicking the control up button" event WF_CONTROL_EVENT_CLICK_SCROLL_DOWN, class=class="str">"cmt">// "Clicking the control down button" event }; class="macro">#define WF_CONTROL_EVENTS_NEXT_CODE(WF_CONTROL_EVENT_TAB_SELECT+class="num">1) class=class="str">"cmt">// The code of the next event after the last graphical element event code enum ENUM_CANV_ELEMENT_PROP_INTEGER { CANV_ELEMENT_PROP_ID = class="num">0, class=class="str">"cmt">// Element ID CANV_ELEMENT_PROP_TYPE, class=class="str">"cmt">// Graphical element type CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH, class=class="str">"cmt">// Visibility scope width CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT, class=class="str">"cmt">// Visibility scope height CANV_ELEMENT_PROP_DISPLAYED, class=class="str">"cmt">// Non-hidden control display flag CANV_ELEMENT_PROP_GROUP, class=class="str">"cmt">// Group the graphical element belongs to CANV_ELEMENT_PROP_ZORDER, class=class="str">"cmt">// Priority of a graphical object for receiving the event of clicking on a chart CANV_ELEMENT_PROP_TAB_PAGE_COLUMN, class=class="str">"cmt">// Tab column index CANV_ELEMENT_PROP_ALIGNMENT, class=class="str">"cmt">// Location of an object inside the control }; class="macro">#define CANV_ELEMENT_PROP_INTEGER_TOTAL(class="num">97) class=class="str">"cmt">// Total number of integer properties class="macro">#define CANV_ELEMENT_PROP_INTEGER_SKIP(class="num">0) class=class="str">"cmt">// Number of integer properties not used in sorting class="macro">#define FIRST_CANV_ELEMENT_DBL_PROP(CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP) class="macro">#define FIRST_CANV_ELEMENT_STR_PROP(CANV_ELEMENT_PROP_INTEGER_TOTAL-CANV_ELEMENT_PROP_INTEGER_SKIP+CANV_ELEMENT_PROP_DOUBLE_TOTAL-CANV_ELEMENT_PROP_DOUBLE_SKIP) enum ENUM_SORT_CANV_ELEMENT_MODE { SORT_BY_CANV_ELEMENT_ID = class="num">0, class=class="str">"cmt">// Sort by element ID };
Canvas 元素排序与系统消息枚举的字段含义
在 MQL5 的 Canvas 图形库里,元素排序靠一组 SORT_BY_CANV_ELEMENT_* 枚举值驱动。比如 SORT_BY_CANV_ELEMENT_DISPLAYED 控制按「非隐藏控件显示标志」排序,SORT_BY_CANV_ELEMENT_ZORDER 则按图表点击接收优先级排,直接决定鼠标事件落到哪层对象。 系统消息枚举 MSG_LIB_SYS_* 用来在库内部报错。MSG_LIB_SYS_FAILED_ENQUEUE_EVENT 表示把事件塞进图表事件队列失败,这种失败在高频自定义事件回测中容易出现,概率随每秒投递事件数上升而增加。 下方代码块列出原文涉及的部分枚举与消息文本定义,含俄英双语文案。想验证的话,在 MT5 的 MetaEditor 里全局搜 SORT_BY_CANV_ELEMENT_DISPLAYED 或 MSG_LIB_SYS_FAILED_ENQUEUE_EVENT,能看到标准库头文件里的实际声明。外汇与贵金属图表上跑这类自定义 Canvas 控件需注意:事件队列阻塞可能引发滑点以外的显示异常,属于高风险调试场景。
SORT_BY_CANV_ELEMENT_TYPE, class=class="str">"cmt">// Sort by graphical element type class=class="str">"cmt">//---... class=class="str">"cmt">//---... SORT_BY_CANV_ELEMENT_VISIBLE_AREA_WIDTH, class=class="str">"cmt">// Sort by visibility scope width SORT_BY_CANV_ELEMENT_VISIBLE_AREA_HEIGHT, class=class="str">"cmt">// Sort by visibility scope height SORT_BY_CANV_ELEMENT_DISPLAYED, class=class="str">"cmt">// Sort by non-hidden control display flag SORT_BY_CANV_ELEMENT_GROUP, class=class="str">"cmt">// Sort by a group the graphical element belongs to SORT_BY_CANV_ELEMENT_ZORDER, class=class="str">"cmt">// Sort by the priority of a graphical object for receiving the event of clicking on a chart class=class="str">"cmt">//---... class=class="str">"cmt">//---... SORT_BY_CANV_ELEMENT_TAB_PAGE_COLUMN, class=class="str">"cmt">// Sort by tab column index SORT_BY_CANV_ELEMENT_ALIGNMENT, class=class="str">"cmt">// Sort by the location of the object inside the control class=class="str">"cmt">//--- Sort by real properties class=class="str">"cmt">//--- Sort by class="type">class="kw">string properties SORT_BY_CANV_ELEMENT_NAME_OBJ = FIRST_CANV_ELEMENT_STR_PROP,class=class="str">"cmt">// Sort by an element object name SORT_BY_CANV_ELEMENT_NAME_RES, class=class="str">"cmt">// Sort by the graphical resource name SORT_BY_CANV_ELEMENT_TEXT, class=class="str">"cmt">// Sort by graphical element text SORT_BY_CANV_ELEMENT_DESCRIPTION, class=class="str">"cmt">// Sort by graphical element description }; class=class="str">"cmt">//+------------------------------------------------------------------+ MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY, class=class="str">"cmt">// Request outside the array MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY, class=class="str">"cmt">// Failed to convert graphical object coordinates to screen ones MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY, class=class="str">"cmt">// Failed to convert time/price coordinates to screen ones MSG_LIB_SYS_FAILED_ENQUEUE_EVENT, class=class="str">"cmt">// Failed to put the event in the chart event queue MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_WIDTH, class=class="str">"cmt">// Visibility scope width MSG_CANV_ELEMENT_PROP_VISIBLE_AREA_HEIGHT, class=class="str">"cmt">// Visibility scope height MSG_CANV_ELEMENT_PROP_DISPLAYED, class=class="str">"cmt">// Non-hidden control display flag MSG_CANV_ELEMENT_PROP_ENABLED, class=class="str">"cmt">// Element availability flag MSG_CANV_ELEMENT_PROP_FORE_COLOR, class=class="str">"cmt">// Default text class="type">class="kw">color for all control objects {"Запрос за пределами массива","Data requested outside the array"}, {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"}, {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"}, {"Не удалось поставить событие в очередь событий графика","Failed to put event in chart event queue"}, {"Ширина области видимости","Width of object visibility area"},
「控件结构体的可见区与显隐字段」
在 MT5 自定义控件类里,SData 结构体把每个图形元素的元信息打包在一起。其中 visible_area_w 与 visible_area_h 两个 int 字段,分别决定该元素在图表上的可见区域宽度与高度,数值单位为像素。 displayed 是 bool 类型,标记非隐藏控件是否真正绘制到画布上。把它和「Element Availability Flag」区分开:后者管交互可用性,displayed 只管渲染开关。 结构体里 name_obj 与 name_res 各预留 64 字节 uchar 数组存对象名和资源名,text 与 descript 给到 256 字节。若你做多语言面板,注意俄文/英文双字典那行只是外部映射表,真正写进结构体的是 uchar 数组,超长会被截断。 调界面时先打印 m_struct_obj.visible_area_h 和 displayed,能快速定位「控件不见了」是坐标偏移还是显隐标志没置位。外汇与贵金属图表插件开发属高风险改造,建议先在模拟账户终端验证。
class="kw">struct SData { class=class="str">"cmt">//--- Object integer properties 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">int visible_area_w; class=class="str">"cmt">// Visibility scope width class="type">int visible_area_h; class=class="str">"cmt">// Visibility scope height class="type">bool displayed; class=class="str">"cmt">// Non-hidden control display flag 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 class="type">uchar descript[class="num">256]; class=class="str">"cmt">// Graphical element description }; SData m_struct_obj; class=class="str">"cmt">// Object structure