DoEasy. 控件 (第 24 部分): 提示(Hint)辅助 WinForms 对象·进阶篇
(2/3)· 当 MQL5 无法改鼠标光标,如何用提示对象在隔板区域示意可拖拽方向
GUI 控件类型的三元映射写法
在 MT5 自建指标或 EA 面板时,常需把图形元素枚举转成可读文本。上面这段是典型的嵌套三元表达式,按 GRAPH_ELEMENT_TYPE_WF_* 枚举逐一比对,命中后由 CMessage::Text() 取对应多语言串。 标准控件先排:基础容器、标签、勾选框、单选钮、按钮,再到各种列表框(普通、带勾选、带按钮),共覆盖 9 类。辅助对象另起一段,含页签头、箭头钮四个方向、上下/左右箭头盒、分割面板与分隔条,合计 12 类。 高亮两行是提示类基类与左移提示,属于易漏的尾部分支。若你抄这段代码做调试输出,漏掉 WF_HINT_* 会导致未知类型直接走默认分支,面板悬停文本会显示空白。外汇与贵金属图表加载此类自定义面板存在脚本冲突导致终端卡顿的高风险,建议先在策略测试器里验证。
class=class="str">"cmt">//--- Standard controls type==GRAPH_ELEMENT_TYPE_WF_COMMON_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_COMMON_BASE) : type==GRAPH_ELEMENT_TYPE_WF_LABEL ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_LABEL) : 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) : type==GRAPH_ELEMENT_TYPE_WF_HINT_BASE ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_BASE) : type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_LEFT) :
◍ 画布元素的相对坐标与父子指针
在 MT5 图形对象封装里,工作流提示类元素(向右、向上、向下移动)会先在三元表达式里映射成对应语言文本,未命中类型则返回 "Unknown",方便在日志里直接看出是哪种引导标记。
相对基准对象的初始偏移由两组存取函数控制:SetCoordXRelativeInit / CoordXRelativeInit 管 X 轴,SetCoordYRelativeInit / CoordYRelativeInit 管 Y 轴,底层字段是 m_init_relative_x 与 m_init_relative_y,均为 int 类型,意味着偏移以像素或栅格步长为单位而非价格。
父子关系上,GetBase 返回当前关联组内的父元素指针 m_element_base;GetMain 则跨所有关联组找主元素——若 m_element_main 为 NULL 就退回 GetObject() 自身。IsMain 仅用 m_element_main==NULL 判定该对象是否充当主元素,调用开销极低。
开 MT5 把这段塞进你自己的 CGCnvElement 派生类,改 m_init_relative_y 正负号,能直接验证提示箭头朝向是否跟着翻转。外汇与贵金属图表对象叠加高风险,参数误用可能让辅助图形错位。
type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_RIGHT) : type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_UP) : type==GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN ? CMessage::Text(MSG_GRAPH_ELEMENT_TYPE_WF_HINT_MOVE_DOWN) : "Unknown" ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the initial shift of the(class="num">1) X and(class="num">2) Y coordinate relative to the base object class="type">void SetCoordXRelativeInit(const class="type">int value) { this.m_init_relative_x=value; } class="type">void SetCoordYRelativeInit(const class="type">int value) { this.m_init_relative_y=value; } class="type">int CoordXRelativeInit(class="type">void) const { class="kw">return this.m_init_relative_x; } class="type">int CoordYRelativeInit(class="type">void) const { class="kw">return this.m_init_relative_y; } class=class="str">"cmt">//--- Return the pointer to the parent element within related objects of the current group CGCnvElement *GetBase(class="type">void) { class="kw">return this.m_element_base; } class=class="str">"cmt">//--- Return the pointer to the parent element within all groups of related objects CGCnvElement *GetMain(class="type">void) { class="kw">return(this.m_element_main==NULL ? this.GetObject() : this.m_element_main); } class=class="str">"cmt">//--- Return the flag indicating that the object is(class="num">1) main, (class="num">2) base class="type">bool IsMain(class="type">void) { class="kw">return this.m_element_main==NULL; }
「从属关系与主基对象 ID 的取法」
在图形元素基类里,IsDependent() 用一行判断 m_element_base 是否非空来确认当前对象是否依附于另一个基准对象:返回 this.m_element_base!=NULL。依附型元素(比如从属于某主图形的子标注)才需要向上追溯主、基对象,独立元素自身就是顶点。 GetMainID() 先调 IsMain(),若本身就是主对象直接返回自己的 ID();否则通过 GetMain() 拿到主指针,非空则返回其 ID(),空则回退 WRONG_VALUE。GetBaseID() 逻辑对称:非依附对象返回自身 ID(),依附对象取 GetBase() 指针并做空判断后返回基准 ID 或 WRONG_VALUE。 下面这段是基类里取主、基 ID 与从属判断的核心片段,注意原文有两处重复的 GetBaseID() 定义(红底行标示),编译前必须删掉其中之一,否则 MT5 会报重定义错误。 别把重名函数当重载 同名 GetBaseID(void) 出现两次且签名完全一致,不是重载而是笔误。MT5 编译器对类内成员函数重名零容忍,直接在 IDE 里标红,删掉重复块再编译能省掉半小时排错。
class="type">bool IsDependent(class="type">void) { class="kw">return this.m_element_base!=NULL; } class=class="str">"cmt">//--- Get the(class="num">1) main and(class="num">2) base object ID class="type">int GetMainID(class="type">void) { if(this.IsMain()) class="kw">return this.ID(); CGCnvElement *main=this.GetMain(); class="kw">return(main!=NULL ? main.ID() : WRONG_VALUE); } class="type">int GetBaseID(class="type">void) { if(!this.IsDependent()) class="kw">return this.ID(); CGCnvElement *base=this.GetBase(); class="kw">return(base!=NULL ? base.ID() : WRONG_VALUE); } class="type">int GetBaseID(class="type">void) { if(!this.IsDependent()) class="kw">return this.ID(); CGCnvElement *base=this.GetBase(); class="kw">return(base!=NULL ? base.ID() : WRONG_VALUE); } class="kw">protected: class=class="str">"cmt">//--- Protected constructor CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, CGCnvElement *main_obj,CGCnvElement *base_obj,
图形元素的相对坐标与事件钩子
在 MT5 自定义图形封装里,每个画布元素都可以带一个「基准对象」,自身坐标不再写死绝对像素,而是相对基准做偏移。上面这段类声明给出了两个 setter/getter 对:SetCoordXRelative 与 CoordXRelative 管水平偏移,SetCoordYRelative 与 CoordYRelative 管垂直偏移,底层就是给 m_shift_coord_x / m_shift_coord_y 赋值和读取。 构造参数里出现了 main_obj 与 base_obj 两个指针:main_obj 通常指该元素归属的主容器,base_obj 才是相对定位的参照物。把基准换成图表上的均线或枢轴位,元素就会跟着锚点走,省去每次重绘算绝对坐标。 virtual void OnChartEvent(...) 是留给子类的图表事件入口。鼠标点击、对象拖拽这类 id 进来后,你可以在派生类里重写它,让元素对交互做反应。外汇与贵金属波动剧烈,用相对坐标绑定的标注在高波动段可能频繁跳动,实盘前建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍确认锚定表现。
const class="type">long chart_id, const class="type">int wnd_num, const class="type">class="kw">string descript, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h); class="kw">public: class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the X coordinate shift relative to the base object class="type">void SetCoordXRelative(const class="type">int value) { this.m_shift_coord_x=value; } class="type">int CoordXRelative(class="type">void) const { class="kw">return this.m_shift_coord_x; } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the Y coordinate shift relative to the base object class="type">void SetCoordYRelative(const class="type">int value) { this.m_shift_coord_y=value; } class="type">int CoordYRelative(class="type">void) const { class="kw">return this.m_shift_coord_y; } class=class="str">"cmt">//--- Event handler class="kw">virtual class="type">void OnChartEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam); class=class="str">"cmt">//--- Parametric constructor CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, CGCnvElement *main_obj,CGCnvElement *base_obj,
◍ 图形元素构造函数的参数与默认初始化
在 MT5 自定义画布类里,图形元素靠一个带十几个入参的构造函数落地。从 element_id、chart_id 到 x/y/w/h 和 colour,这套签名把元素绑定到具体图表窗口和像素坐标,opacity 用 uchar 控制透明度,movable 默认 true 代表用户可拖拽,activity 默认 true 表示响应事件,redraw 默认 false 避免无谓重绘。 下面是节选的构造函数声明与默认构造函数本体,直接对应上面那些字段的初值逻辑。 默认构造函数里把 m_shadow 置为 false,并用 ChartGetInteger 取当前图表背景色填给 m_chart_color_bg;m_type 写死 OBJECT_DE_TYPE_GELEMENT 标明类型,m_element_main 与 m_element_base 都设 NULL,坐标偏移 m_shift_coord_x/y 清零。开 MT5 把这段贴进自己的画布类,断点看构造后成员值,能确认元素基类不会自带阴影和偏移。
const class="type">int element_id, const class="type">int element_num, const class="type">long chart_id, const class="type">int wnd_num, const class="type">class="kw">string descript, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">color colour, const class="type">uchar opacity, const class="type">bool movable=true, const class="type">bool activity=true, const class="type">bool redraw=false); class=class="str">"cmt">//--- Default constructor CGCnvElement() : m_shadow(false),m_chart_color_bg((class="type">color)::ChartGetInteger(::ChartID(),CHART_COLOR_BACKGROUND)) { this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=NULL; this.m_element_base=NULL; this.m_shift_coord_x=class="num">0; this.m_shift_coord_y=class="num">0; }
「图形元素的析构与带参构造落点」
CGCnvElement 的析构函数只做一件事:调用 this.m_canvas.Destroy() 释放绑定的画布资源,避免 MT5 指标退出时残留 GDI 对象句柄。 构造阶段先把两个指针置空:m_element_main 与 m_element_base 都赋为 NULL,这是在参数列表执行前就切断悬空引用的习惯写法,能在后续主从对象绑定失败时不至于访问野指针。 带参构造函数一口气收了 19 个入参,从元素类型 element_type、主对象 main_obj、基对象 base_obj,到坐标 x/y、尺寸 w/h、颜色 colour、透明度 opacity,再到 movable / activity / redraw 三个布尔开关,默认分别是 true、true、false。 初始化列表里 m_shadow(false) 先把阴影关掉,函数体内紧接着 SetTypeElement(element_type) 设类型,m_type 写死为 OBJECT_DE_TYPE_GELEMENT,最后才把传入的 main_obj 落到 m_element_main 上。开 MT5 把这段贴进自定义类,断点停在 this.m_element_main=main_obj 那行,能直接看到主对象挂靠是否成功。
class=class="str">"cmt">//--- Destructor ~CGCnvElement() { this.m_canvas.Destroy(); } class=class="str">"cmt">//+------------------------------------------------------------------+ this.m_element_main=NULL; this.m_element_base=NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Parametric constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, CGCnvElement *main_obj,CGCnvElement *base_obj, const class="type">int element_id, const class="type">int element_num, const class="type">long chart_id, const class="type">int wnd_num, const class="type">class="kw">string descript, const class="type">int x, const class="type">int y, const class="type">int w, const class="type">int h, const class="type">color colour, const class="type">uchar opacity, const class="type">bool movable=true, const class="type">bool activity=true, const class="type">bool redraw=false) : m_shadow(false) { this.SetTypeElement(element_type); this.m_type=OBJECT_DE_TYPE_GELEMENT; this.m_element_main=main_obj; }