自定义图形控件。第一部分:创建简单控件·进阶篇
📘

自定义图形控件。第一部分:创建简单控件·进阶篇

第 2/3 篇

用 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 属高风险操作,参数误写可能让对象满屏漂移,建议在模拟账户先验证。

MQL5 / C++
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() 则是删了再建——控件轻量,这么搞够用。

MQL5 / C++
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,能直接看到背景变色而无需重开图表。外汇与贵金属波动剧烈,这类面板只做信息呈现,不构成任何方向暗示。

MQL5 / C++
  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 拿到输入框变更的最直接钩子。外汇与贵金属波动剧烈,这类边界钳制能降低误设参数引发的滑点风险,但无法消除交易本身的高风险属性。

MQL5 / C++
  {  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 里动态排布多个控件。

MQL5 / C++
{ 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;
}

常见问题

用 CWorkPiece 做容器把子控件按固定间距横向压成一行,初始化时统一设左间距和宽度即可避免重叠。
建一个带编辑框的自定义面板,在字段初始化时开允许输入标志,并把显隐逻辑绑到鼠标点击事件上。
可以,小布能按你给的字段规则跑一遍显隐与边界约束,直接标出哪格取值越界或事件没响应。
在事件处理里加模式标记,文本模式屏蔽数字格式化、数值模式拦截非数字字符,分别走不同回写分支。
在取值函数末尾加边界约束判断,越界时强制夹回限值并触发一次重绘,别只在界面层做提示。