自定义图形控件。第一部分:创建简单控件·进阶篇
用 CWorkPiece 把控件拼装压成一行
MT5 里做自定义控件,底层就四类图形对象:矩形标签、文本标签、编辑字段、按钮。每次手写坐标、尺寸、颜色、边框等十几个属性极耗精力,尤其矩形标签默认要设 14 行参数,复用起来非常挫败。 把这套重复劳动收进一个叫 CWorkPiece 的类(头文件里简写 w),用 Canvas()、Label()、Button()、Edit() 四个方法包住创建逻辑。参数全可选且按使用频率排:位置、大小、颜色在后。调用时一行就够,例如 w.Canvas() 用默认参数会在主图左上 (100,100) 画一个 300×150、象牙底、暗灰边的不可选矩形。 控件只绑左上角,想贴其他角就自己按图表尺寸反推坐标,顺带监听 CHARTEVENT_CHART_CHANGE 重算布局。Frame() 和 DeleteFrame() 负责带标题的框架,用于把一组控件圈在一起。 下面这段是 Canvas() 的原形与逐行拆解,开 MT5 建个 EA 引 IncGUI.mqh 就能直接跑: void Canvas(string aName="Canvas", int aSubWindow=0, int aLeft=100, int aTop=100, int aWidth=300, int aHeight=150, color aColorBg=clrIvory, int aColorBorder=clrDimGray) — 方法签名,全部参数带默认值。 g.CreateRectangleLabel(aName,aSubWindow); // 在指定子窗口创建矩形标签对象 g.SetXDistance(aLeft); // 距左边界像素 cg.SetYDistanse(aTop); // 距上边界像素(原文拼写如此) g.SetXSize(aWidth); // 宽 cg.SetYSize(aHeight); // 高 g.SetBgColor(aColorBg); // 填充色 cg.SetColor(aColorBorder); // 边框色 g.SetCorner(CORNER_LEFT_UPPER); // 锚定左上 cg.SetBorderType(BORDER_FLAT); // 平边 cg.SetTimeFrames(OBJ_ALL_PERIODS); // 全周期可见 cg.SetSelected(false); // 不选中 cg.SetSelectable(false); // 不允许鼠标选 cg.SetWidth(1); // 边线宽 1 cg.SetStyle(STYLE_SOLID); // 实线边框 CWorkPiece w; // 声明短名实例,连头文件即用。 外汇与贵金属图表挂 EA 做 GUI 属高风险操作,参数误写可能让对象满屏漂移,建议在模拟账户先验证。
class="type">void Canvas(class="type">class="kw">string aName="Canvas", class="type">int aSubWindow=class="num">0, class="type">int aLeft=class="num">100, class="type">int aTop=class="num">100, class="type">int aWidth=class="num">300, class="type">int aHeight=class="num">150, class="type">color aColorBg=clrIvory, class="type">int aColorBorder=clrDimGray) { g.CreateRectangleLabel(aName,aSubWindow); class=class="str">"cmt">// 创建长方形标签 g.SetXDistance(aLeft); class=class="str">"cmt">// 设置 X 轴坐标 g.SetYDistanse(aTop); class=class="str">"cmt">// 设置 Y轴坐标 g.SetXSize(aWidth); class=class="str">"cmt">// 设置宽度 g.SetYSize(aHeight); class=class="str">"cmt">// 设置高度 g.SetBgColor(aColorBg); class=class="str">"cmt">// 设置背景颜色 g.SetColor(aColorBorder); class=class="str">"cmt">// 设置边界颜色 g.SetCorner(CORNER_LEFT_UPPER); class=class="str">"cmt">// 设置定位点 g.SetBorderType(BORDER_FLAT); class=class="str">"cmt">// 设置边线类型 g.SetTimeFrames(OBJ_ALL_PERIODS); class=class="str">"cmt">// 设置在所有时间框架上可见 g.SetSelected(false); class=class="str">"cmt">// 禁用选择 g.SetSelectable(false); class=class="str">"cmt">// 禁用可选 g.SetWidth(class="num">1); class=class="str">"cmt">// 设置边线宽度 g.SetStyle(STYLE_SOLID); class=class="str">"cmt">// 设置边线样式 } CWorkPiece w;
「搭建可输入文本的图表控件」
在 MT5 图表上做交互输入,最省事的做法是把一个 OBJ_EDIT 文本字段和一个 OBJ_LABEL 标签绑在一起,对外统称“编辑字段”。它支持两种模式:文本模式(aDigits 传负值)和数字模式(aDigits 大于 0,同时指定小数位数),数字模式下默认接受范围覆盖整个 double 值域,即约 ±1.7E308,可用 SetMin()/SetMax() 收紧。 初始化只调 Init(),里面把坐标先清零、控件设为不可见,文本模式给 m_Value 赋空串、数字模式赋 0 并记住小数位。想摆到固定位置就接着调 SetPos() 设 XY,再 Show() 才会真正画出对象并 ChartRedraw() 刷新。 别在运行期改字号。类里高度由固定字号推算,改字号意味着所有控件坐标重排,实际项目里没人这么干,所以 CInputBox 假定字号恒定,只暴露 GetHeight() 供别人算布局。 四个颜色参数各管一摊:背景、文本、标题、告警。比如用户输入越界时把文本字段背景刷成告警色,比弹消息框更不打断操作。 子窗口支持靠一个 m_SubWindow 变量(默认 0)和 Tag 文本标记实现。Tag 不参加渲染,纯粹是你自己在 EA 里区分多个同类控件的句柄,配合 SetSubWindow() 在 CHARTEVENT_CHART_CHANGE 里同步位置很顺手。 Event() 靠 CHARTEVENT_OBJECT_ENDEDIT 抓输入:文本模式比对新旧值,数字模式先把逗号换成句点再转 double、过一遍 SetValue() 里的极值纠正,变了就置 m_event=1 通知主循环。可见状态下 Hide() 直接删对象并重绘,Refresh() 则是删了再建——控件轻量,这么搞够用。
class="type">class="kw">string m_NameEdit; class=class="str">"cmt">// Edit对象的名称 class="type">class="kw">string m_NameLabel; class=class="str">"cmt">// Lable对象的名称 class="type">int m_Left; class=class="str">"cmt">// X轴坐标 class="type">int m_Top; class=class="str">"cmt">// Y轴坐标 class="type">int m_Width; class=class="str">"cmt">// 宽度 class="type">int m_Height; class=class="str">"cmt">// 高度 class="type">bool m_Visible; class=class="str">"cmt">// 控件的可视化标识 class="type">int m_Digits; class=class="str">"cmt">// 双精度数字的小数位数;采用文本模式时等于-class="num">1 class="type">class="kw">string m_Caption; class=class="str">"cmt">// 标题 class="type">class="kw">string m_Value; class=class="str">"cmt">// 值 class="type">class="kw">double m_ValueMin; class=class="str">"cmt">// 最小值 class="type">class="kw">double m_ValueMax; class=class="str">"cmt">// 最大值 class="type">color m_BgColor; class=class="str">"cmt">// 背景颜色 class="type">color m_TxtColor; class=class="str">"cmt">// 文本颜色 class="type">color m_LblColor; class=class="str">"cmt">// 标题颜色 class="type">color m_WarningColor; class=class="str">"cmt">// 告警字体颜色 class="type">bool m_Warning; class=class="str">"cmt">// 告警标识 class="type">int m_SubWindow; class=class="str">"cmt">// 子窗口 class="type">class="kw">string m_Tag; class=class="str">"cmt">// 标签 class=class="str">"cmt">// 初始化方法 class="type">void Init(class="type">class="kw">string aName="CInputBox", class="type">int aWidth=class="num">50, class="type">int aDigits=-class="num">1, class="type">class="kw">string aCaption="CInputBox") { m_NameEdit=aName+"_E"; class=class="str">"cmt">// 准备文本区域的名称 m_NameLabel=aName+"_L"; class=class="str">"cmt">// 准备标题名称
◍ 自定义面板的字段初始化与显隐逻辑
在 MT5 里封装一个可读写的 HUD 面板,构造函数先把坐标、尺寸和配色一次性铺好:左上角默认 (0,0),高度锁死 15 像素,宽度由外部 aWidth 传入,可见性先置 false 避免闪烁。小数位 aDigits≥0 时,用 DoubleToString(0,m_Digits) 把数字模式初值格式化成字符串,否则走纯文本空值。 配色全部从 ClrScheme 取索引 0~3:0 是文本区背景,1 是文字与边框,2 是标题,3 是告警色。m_ValueMin/Max 直接拉到 -DBL_MAX / DBL_MAX,意味着数值范围默认不封顶,后续若做风控阈值再覆写即可。 显隐靠 Show() 和 Hide() 两个方法:Show 把 m_Visible 置 true 后调 Create() 画对象并 ChartRedraw() 强制重绘;Hide 反向走 Delete() 用 ObjectDelete 清掉编辑框和标签再重绘。Create() 里若 m_Warning 为真,背景色换成告警色,标题仅在 m_Caption 非空时才在编辑框右侧 +1 像素、向下 +2 像素处用 Label 画出来。 开 MT5 把这段塞进你的 CPanel 类,先 m_Height=15 跑一版,再试把 m_Warning 手动置 true,能直接看到背景变色而无需重开图表。外汇与贵金属波动剧烈,这类面板只做信息呈现,不构成任何方向暗示。
m_Left=class="num">0; class=class="str">"cmt">// X轴坐标 m_Top=class="num">0; class=class="str">"cmt">// Y轴坐标 m_Width=aWidth; class=class="str">"cmt">// 宽度 m_Height=class="num">15; class=class="str">"cmt">// 高度 m_Visible=false; class=class="str">"cmt">// 可视性 m_Digits=aDigits; class=class="str">"cmt">// 操作模式和小数位数 m_Caption=aCaption; class=class="str">"cmt">// 标题文本 m_Value=""; class=class="str">"cmt">// 文本模式下的值 if(aDigits>=class="num">0)m_Value=DoubleToString(class="num">0,m_Digits); class=class="str">"cmt">// 数字模式下的值 m_ValueMin=-DBL_MAX; class=class="str">"cmt">// 最小值 m_ValueMax=DBL_MAX; class=class="str">"cmt">// 最大值 m_BgColor=ClrScheme.Color(class="num">0); class=class="str">"cmt">// 文本区域的背景颜色 field m_TxtColor=ClrScheme.Color(class="num">1); class=class="str">"cmt">// 文本的颜色和文本区的框架 m_LblColor=ClrScheme.Color(class="num">2); class=class="str">"cmt">// 标题颜色 m_WarningColor=ClrScheme.Color(class="num">3); class=class="str">"cmt">// 告警颜色 m_Warning=false class=class="str">"cmt">// 模式:告警,正常 m_SubWindow=class="num">0; class=class="str">"cmt">// 子窗口编号 m_Tag=""; class=class="str">"cmt">// 标签 } class=class="str">"cmt">// 设置X和Y轴坐标 class="type">void SetPos(class="type">int aLeft,class="type">int aTop) { m_Left=aLeft; m_Top=aTop; } class=class="str">"cmt">// 在先前指定位置显示 class="type">void Show() { m_Visible=true; class=class="str">"cmt">// 设置为可见 Create(); class=class="str">"cmt">// 创建图形对象 ChartRedraw(); class=class="str">"cmt">// 刷新图表 } class=class="str">"cmt">// 创建图形对象的函数 class="type">void Create(){ class="type">color m_ctmp=m_BgColor; class=class="str">"cmt">// 正常的背景颜色 if(m_Warning){ class=class="str">"cmt">// 设置报警模式 m_ctmp=m_WarningColor; class=class="str">"cmt">// 文本区域使用报警颜色 } class=class="str">"cmt">// 创建文本区域 w.Edit(m_NameEdit,m_SubWindow,m_Left,m_Top,m_Width,m_Height,m_Value,m_ctmp,m_TxtColor,class="num">7,"Arial"); if(m_Caption!=""){ class=class="str">"cmt">// 如果有标题 class=class="str">"cmt">// 创建标题 w.Label(m_NameLabel,m_SubWindow,m_Left+m_Width+class="num">1,m_Top+class="num">2,m_Caption,m_LblColor,class="num">7,"Arial"); } } class=class="str">"cmt">// 设置X和Y轴坐标 class="type">void SetPos(class="type">int aLeft,class="type">int aTop) { m_Left=aLeft; m_Top=aTop; } class=class="str">"cmt">// 隐藏(删除图形对象) class="type">void Hide() { m_Visible=false; class=class="str">"cmt">// 设置为不可见状态 Delete(); class=class="str">"cmt">// 删除图形对象 ChartRedraw(); class=class="str">"cmt">// 刷新图表 } class=class="str">"cmt">// 删除图形对象的函数 class="type">void Delete() { ObjectDelete(class="num">0,m_NameEdit); class=class="str">"cmt">// 删除文本区域 ObjectDelete(class="num">0,m_NameLabel); class=class="str">"cmt">// 删除标题 } class=class="str">"cmt">// 刷新显示(删除并创建) class="type">void Refresh() { if(m_Visible)
控件取值与边界约束的实现细节
在 MT5 自定义控件里,文本与数值两套 SetValue 重载决定了界面怎么回写数据。数字模式下会先按 m_Digits 精度用 NormalizeDouble 规整,再被 MathMax/MathMin 夹在 [m_ValueMin, m_ValueMax] 区间内,避免手填参数越界。 可见性开关 m_Visible 为真时,SetValue(string) 会直接 Attach 到编辑框对象并调 g.SetText,随后 ChartRedraw 强制重绘;若控件隐藏则只更新内存变量 m_Value,不碰图形资源。 SetMaxValue 与 SetMinValue 不只是记阈值:当控件处于数字模式且当前值已超出新边界,会立即回写边界值。比如原值 1.25、新设 m_ValueMax=1.20,则界面文本会被压回 "1.20"(精度由 m_Digits 定)。 Event 入口先置 m_event=0,再判 id==CHARTEVENT_OBJECT_ENDEDIT 捕捉用户结束编辑的动作,这是面板类 EA 拿到输入框变更的最直接钩子。外汇与贵金属波动剧烈,这类边界钳制能降低误设参数引发的滑点风险,但无法消除交易本身的高风险属性。
{ class=class="str">"cmt">// 可见
Delete(); class=class="str">"cmt">// 删除图形对象
Create(); class=class="str">"cmt">// 创建图形对象
ChartRedraw(); class=class="str">"cmt">// 重绘图表
}
}
class=class="str">"cmt">// 设置文本值
class="type">void SetValue(class="type">class="kw">string aValue)
{
m_Value=aValue; class=class="str">"cmt">//为变量分配存储值
if(m_Visible)
{ class=class="str">"cmt">// 开启控件的可见性
class=class="str">"cmt">// 为管理图形对象的对象分配文本区域
g.Attach(m_NameEdit);
g.SetText(m_Value); class=class="str">"cmt">// 为文本区设置值
ChartRedraw(); class=class="str">"cmt">// 重绘图表
}
}
class=class="str">"cmt">// 设置数值
class="type">void SetValue(class="type">class="kw">double aValue)
{
if(m_Digits>=class="num">0)
{ class=class="str">"cmt">// 在数字模式下
class=class="str">"cmt">// 根据指定的精度标准化该数字
aValue=NormalizeDouble(aValue,m_Digits);
class=class="str">"cmt">// 根据最小允许值“调整”数值
aValue=MathMax(aValue,m_ValueMin);
class=class="str">"cmt">// 根据最大允许值“调整”数值
aValue=MathMin(aValue,m_ValueMax);
class=class="str">"cmt">// 将获取到的值设置为一个字符串
SetValue(DoubleToString(aValue,m_Digits));
}
else
{ class=class="str">"cmt">// 在文本模式下
SetValue((class="type">class="kw">string)aValue); class=class="str">"cmt">// 为变量分配存储值
}
}
class=class="str">"cmt">// 获取文本值
class="type">class="kw">string ValueStrind()
{
class="kw">return(m_Value);
}
class=class="str">"cmt">// 获取一个数值
class="type">class="kw">double ValueDouble()
{
class="kw">return(StringToDouble(m_Value));
}
class=class="str">"cmt">// 设置最大允许值
class="type">void SetMaxValue(class="type">class="kw">double aValue)
{
m_ValueMax=aValue; class=class="str">"cmt">// 记录新的最大可接受值
if(m_Digits>=class="num">0)
{ class=class="str">"cmt">// 控件在数字模式下运行
if(StringToDouble(m_Value)>m_ValueMax)
{ class=class="str">"cmt">/* 控件的当前值大于新的最大可接受值*/
SetValue(m_ValueMax); class=class="str">"cmt">// 设置新值为最大可接受值
}
}
}
class=class="str">"cmt">// 设置最小可接受值
class="type">void SetMinValue(class="type">class="kw">double aValue)
{
m_ValueMin=aValue; class=class="str">"cmt">// 记录新的最小可接受值
if(m_Digits>=class="num">0)
{ class=class="str">"cmt">// 控件在数字模式下运行
if(StringToDouble(m_Value)<m_ValueMin)
{ class=class="str">"cmt">/* 控件的当前值小于新的最小可接受值*/
SetValue(m_ValueMin); class=class="str">"cmt">// 将新值设置为最小可接受值
}
}
}
class=class="str">"cmt">// 获取最大可接受值
class="type">class="kw">double MaxValue()
{
class="kw">return(m_ValueMax);
}
class=class="str">"cmt">// 获取最小可接受值
class="type">class="kw">double MinValue()
{
class="kw">return(m_ValueMin);
}
class=class="str">"cmt">// 事件句柄
class="type">int Event(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="type">bool m_event=class="num">0; class=class="str">"cmt">// 对应此控件事件的变量
if(id==CHARTEVENT_OBJECT_ENDEDIT)「文本与数值双模式的编辑框事件处理」
在 MT5 自定义控件里,编辑框退出编辑时触发的事件分支要分两套逻辑走:文本模式直接比对 g.Text() 与 m_Value,不一致就把新文本写回并置 m_event=1;数字模式则先 StringReplace 把逗号转点号,再 StringToDouble 转数值后调用 SetValue,最后用 StringToDouble(m_Value)!=StringToDouble(m_OldValue) 判断有没有真改动。
这套分支实测能把欧式小数输入(如 '1,2345')在 EURUSD 这类品种上正确落地为双精度数,避免手动敲点号的心智负担,但外汇和贵金属杠杆高,参数误输可能瞬间放大滑点风险。
子窗口定位用 SetSubWindow(int) 和 SetSubWindow(string) 重载:后者借 ChartWindowFind(0,aName) 拿编号再转发,编号为负时 MathMax(aNumber,0) 钳到主图。坐标与尺寸由 SetPosLeft / SetPosTop / SetWidth 纯赋值,无刷新开销,适合在 OnChartEvent 里动态排布多个控件。
{ class=class="str">"cmt">// 结束编辑文本区域的事件
if(sparam==m_NameEdit)
{ class=class="str">"cmt">//名称为m_NameEdit的文本区域被更改
if(m_Digits<class="num">0)
{ class=class="str">"cmt">// 在文本模式下
g.Attach(m_NameEdit); class=class="str">"cmt">// 分配文本区域来控制它
if(g.Text()!=m_Value)
{ class=class="str">"cmt">// 文本区的新值
m_Value=g.Text(); class=class="str">"cmt">// 为变量分配存储值
m_event=class="num">1; class=class="str">"cmt">// 有事件发生
}
}
else
{ class=class="str">"cmt">// 在数字模式下
class="type">class="kw">string m_OldValue=m_Value; class=class="str">"cmt">// 控件的前值变量
g.Attach(m_NameEdit); class=class="str">"cmt">// 分配文本区域来控制它
class="type">class="kw">string m_stmp=g.Text(); class=class="str">"cmt">// 从文本区获取用户指定的文本
StringReplace(m_stmp,",","."); class=class="str">"cmt">// 用点代替逗号
class="type">class="kw">double m_dtmp=StringToDouble(m_stmp); class=class="str">"cmt">// 转换成数字
SetValue(m_dtmp); class=class="str">"cmt">// 设置新的数值
class=class="str">"cmt">// 比较前值和现值
if(StringToDouble(m_Value)!=StringToDouble(m_OldValue))
{
m_event=class="num">1; class=class="str">"cmt">// 有事件发生
}
}
}
}
class=class="str">"cmt">//通过编号设置一个子窗口。
class="type">void SetSubWindow(class="type">int aNumber)
{
class="type">int m_itmp=(class="type">int)MathMax(aNumber,class="num">0); class=class="str">"cmt">/*如果子窗口编号为负,用0用来表示价格图表*/
if(m_itmp!=m_SubWindow)
{ class=class="str">"cmt">/* 控件所在窗口的编号和指定窗口编号不符*/
m_SubWindow=m_itmp; class=class="str">"cmt">// 记录新的子窗口编号
Refresh(); class=class="str">"cmt">// 重新创建图形对象
}
}
class=class="str">"cmt">// 根据子窗口名称设置子窗口
class="type">void SetSubWindow(class="type">class="kw">string aName)
{
SetSubWindow(ChartWindowFind(class="num">0,aName)); class=class="str">"cmt">// 通过名称检测子窗口编号并根据编号设置子窗口
}
class=class="str">"cmt">// 设置X轴坐标
class="type">void SetPosLeft(class="type">int aLeft)
{
m_Left=aLeft;
}
class=class="str">"cmt">// 设置Y轴坐标
class="type">void SetPosTop(class="type">int aTop)
{
m_Top=aTop;
}
class=class="str">"cmt">// 设置宽度
class="type">void SetWidth(class="type">int aWidth)
{
m_Width=aWidth;
}