如何创建任意复杂度的图形面板·综合运用
🧩

如何创建任意复杂度的图形面板·综合运用

(3/3)·从零理解继承结构与事件宏,把标准库控件改造成顺手的交易面板

新手友好 第 3/3 篇
很多 EA 开发者避开图形界面,只是因为标准库只给了方法列表却没有讲清结构。其实基于 CAppDialog 拼面板比想象中轻,先弄懂对象继承和销毁顺序就能少踩大半坑。

遍历控件改底色再删客户区

在 MT5 用 CAppDialog 搭界面时, OnInit 里先 Create(0,"AppWindow",0,20,20,360,324) 把窗口定在左上角偏移 20 像素、宽 360 高 324,失败就直接 INIT_FAILED,这尺寸在 1920×1080 屏上约占右下 1/6 面积,方便边看盘边调参数。 下面这段循环是核心:用 ControlsTotal() 拿控件总数,挨个 Control(i) 取对象,Name() 拿到名字后 PrintFormat 打出「%d 是 %s」。名字里带 Client 的转成 CWndClient 刷成红底,带 Back 的转 CPanel 刷成绿底,每次改完 ChartRedraw() 强制重绘,日志里能看到 client.ColorBackground(clrRed) 或 panel.ColorBackground(clrGreen) 的回显。 黄底那几行跑完遍历后,AppWindow.Delete(myclient) 把刚改红底的客户区直接删掉——也就是说红底刚画上就被移除,实际肉眼大概率只留绿底面板。外汇和贵金属图表挂这类 EA 属高风险操作,建议先在策略测试器里空跑验证控件名匹配逻辑,再上真实tick。 OnDeinit 里 Destroy(reason) 释放对话框,OnChartEvent 把图表事件全转给 AppWindow.ChartEvent,界面交互才不会失联。复制下面代码到 MQ5 文件编译,能在专家日志里确认控件索引与命名规律。

MQL5 / C++
class="macro">#include <Controls\Dialog.mqh>
CAppDialog AppWindow;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"AppWindow",class="num">0,class="num">20,class="num">20,class="num">360,class="num">324))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//---
   class="type">int total=AppWindow.ControlsTotal();
   CWndClient*myclient;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CWnd*obj=AppWindow.Control(i);
      class="type">class="kw">string name=obj.Name();
      PrintFormat("%d 是 %s",i,name);
      class=class="str">"cmt">//--- 颜色
      if(StringFind(name,"Client")>class="num">0)
        {
         CWndClient *client=(CWndClient*)obj;
         client.ColorBackground(clrRed);
         myclient=client;
         Print("client.ColorBackground(clrRed);");
         ChartRedraw();
        }
      class=class="str">"cmt">//---
      if(StringFind(name,"Back")>class="num">0)
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrGreen);
         Print("panel.ColorBackground(clrGreen);");
         ChartRedraw();
        }
      }
   AppWindow.Delete(myclient);
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易终止函数                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易图表事件函数                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「在 OnInit 里给客户端区刷红底」

想在 MT5 面板刚起来时就改掉客户区背景,最直接的位置是 OnInit。下面这段把 CAppDialog 建出来后,立刻遍历所有子控件,按名字命中 Client 就把背景涂成红、命中 Back 就涂绿,然后 Sleep(5000) 再销毁客户区,最后跑 Run()。 遍历逻辑靠 ControlsTotal 拿总数,Control(i) 取指针,Name() 拿控件名。StringFind(name,"Client")>0 说明名字里带 Client,此时把 obj 强转成 CWndClient* 再调 ColorBackground(clrRed),紧接着 ChartRedraw() 才会立刻在图上可见。 一个容易踩的坑:Sleep(5000) 会卡住 OnInit 线程 5 秒,实盘加载 EA 时界面会停顿半秒级以外的明显阻塞。外汇与贵金属杠杆高,加载期卡顿可能错过瞬时报价,建议仅用于回测或离线验证。 让小布替你跑这套 把 Sleep 改成注释,直接在策略测试器里加载,看专家日志里打出的 "%d 是 %s" 顺序,能确认 Client 和 Back 控件的索引号,方便后续精准改色而不用全量遍历。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 LearnCAppDialog_2.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                       [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Controls\Dialog.mqh>
CAppDialog AppWindow;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"AppWindow",class="num">0,class="num">20,class="num">20,class="num">360,class="num">324))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//---
   class="type">int total=AppWindow.ControlsTotal();
   CWndClient*myclient;
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CWnd*obj=AppWindow.Control(i);
      class="type">class="kw">string name=obj.Name();
      PrintFormat("%d 是 %s",i,name);
      class=class="str">"cmt">//--- 颜色
      if(StringFind(name,"Client")>class="num">0)
        {
         CWndClient *client=(CWndClient*)obj;
         client.ColorBackground(clrRed);
         myclient=client;
         Print("client.ColorBackground(clrRed);");
         ChartRedraw();
        }
      class=class="str">"cmt">//---
      if(StringFind(name,"Back")>class="num">0)
        {
         CPanel *panel=(CPanel*) obj;
         panel.ColorBackground(clrGreen);
         Print("panel.ColorBackground(clrGreen);");
         ChartRedraw();
        }
     }
   Sleep(class="num">5000);
   myclient.Destroy();
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易终止函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把图表事件转交给应用窗口

在 MQL5 的 EA 框架里,OnChartEvent 是捕获 MT5 图表交互的唯一入口,鼠标点击、键盘按键、自定义图形对象事件都会从这里进来。 上面这段把四个入参原封不动交给 AppWindow.ChartEvent,等于把图表层的原始事件流桥接到了自定义应用窗口对象,后续 UI 响应逻辑可以全在 AppWindow 内写,EA 主体保持干净。 实盘里若你用 Canvas 或自定义面板做贵金属盯盘,这种转发写法能让你在 AppWindow 里直接处理 CHARTEVENT_OBJECT_CLICK,不必在 EA 里堆一堆 switch 分支。外汇与贵金属杠杆高,事件驱动面板若误触发下单,风险可能瞬间放大,转发前务必在 AppWindow 内做参数校验。

MQL5 / C++
class=class="str">"cmt">//| EA 交易图表事件函数                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id,      class=class="str">"cmt">// 事件 ID
                 const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                 const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                 const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

往面板里塞两个 CButton 实例

在已有 CAppDialog 面板基础上加按钮,先想清楚布局再写代码。按钮距客户区上边和左边都留 11 像素(INDENT_TOP / INDENT_LEFT),控件之间水平间隙设 5 像素(CONTROLS_GAP_X),单个按钮宽 100、高 20(BUTTON_WIDTH / BUTTON_HEIGHT)。 要用 CButton 得先 #include <Controls\Button.mqh>。为缩减示例篇幅,作者把 m_button1、m_button2 两个 CButton 实例放在全局声明——实际工程里这不是好做法,因为实例方法会暴露给 EA 全部作用域,但跑通逻辑够用。 按钮光 Create() 出来是游离的,必须调 CDialog::Add() 把它挂进面板客户区,否则点不到也管不着。OnInit() 里要校验 CreateButton1() / CreateButton2() 的返回值;OnDeinit() 销毁面板,OnChartEvent() 把事件透传给表单,这三处漏掉按钮就残了。 下面这段是声明与常量的核心骨架,直接抄进 MT5 能编译出两个空白按钮位,再补 Text() 和 Add() 就能见形。外汇贵金属面板开发属高风险定制,参数改错可能让 EA 在实盘卡死,建议先用模拟盘验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                          AppWindowTwoButtons.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                 [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.001"
class="macro">#class="kw">property description "控制面板与对话框. 演示 CButton 类"
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\Button.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定义                                                            | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 缩进和距离
class="macro">#define INDENT_LEFT(class="num">11)      class=class="str">"cmt">// 与左边的缩进 (包括允许的边框宽度)
class="macro">#define INDENT_TOP(class="num">11)      class=class="str">"cmt">// 与顶部的缩进 (包括允许的边框宽度)
class="macro">#define CONTROLS_GAP_X(class="num">5)       class=class="str">"cmt">// X 坐标上的距离
class=class="str">"cmt">//--- 用于按钮
class="macro">#define BUTTON_WIDTH(class="num">100)     class=class="str">"cmt">//  X 坐标上的大小
class="macro">#define BUTTON_HEIGHT(class="num">20)      class=class="str">"cmt">// Y 坐标上的大小
class=class="str">"cmt">//---
class="macro">#define BUTTON_HEIGHT(class="num">20)      class=class="str">"cmt">// Y 坐标上的大小
class=class="str">"cmt">//---
CAppDialog       AppWindow;
CButton          m_button1;                 class=class="str">"cmt">// 按钮对象
CButton          m_button2;                 class=class="str">"cmt">// 按钮对象
class=class="str">"cmt">//+------------------------------------------------------------------+

「EA 初始化里把两个按钮挂进 AppWindow」

在 MT5 里用 CAppDialog 派生类写 EA 界面时,OnInit 只做三件事:建窗、建控件、跑消息循环。窗体用 AppWindow.Create(0,"含有两个按钮的 AppWindow",0,40,40,380,344) 起手,左顶点落在屏幕 (40,40),宽 380 高 344 像素;任一步返回 false 就直接 INIT_FAILED,MT5 终端会在专家日志里报初始化失败。 控件必须逐个挂进窗体,否则图表上什么都看不到。下面这段是按钮 1 的坐标推导:x1=INDENT_LEFT 即 11 像素,y1=INDENT_TOP 也是 11 像素;x2 = 11+100 = 111,y2 = 11+20 = 32,也就是按钮占 100×20 像素的矩形。m_button1.Create 完还要 Text("Button1") 设标签,最后 AppWindow.Add(m_button1) 才真正进窗体子树。 注意原文里有一段被标黄的代码 if(!AppWindow.Add(m_button1)) return(false); 出现在 OnInit 右大括号之外、CreateButton1 定义之前,这是典型的拷贝错位。真要编译过,这行必须收进 CreateButton1 函数体内,跟在 Text 设置之后。外汇与贵金属 EA 带界面时滑点与时延敏感,界面初始化失败可能让策略在重大数据行情里哑火,属于高风险环节。 OnDeinit 里调 AppWindow.Destroy(reason) 释放窗体,前面顺手 Comment("") 清掉图表左上角残留文字,避免退出后还留着旧提示。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!AppWindow.Create(class="num">0,"含有两个按钮的 AppWindow",class="num">0,class="num">40,class="num">40,class="num">380,class="num">344))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 创建依赖的控件
   if(!CreateButton1())
      class="kw">return(class="kw">false);
   if(!CreateButton2())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 运行应用程序
   AppWindow.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }

class="type">bool CreateButton1(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT;      class=class="str">"cmt">// x1      = class="num">11  像素
   class="type">int y1=INDENT_TOP;       class=class="str">"cmt">// y1      = class="num">11  像素
   class="type">int x2=x1+BUTTON_WIDTH;  class=class="str">"cmt">// x2 = class="num">11 + class="num">100 = class="num">111 像素
   class="type">int y2=y1+BUTTON_HEIGHT; class=class="str">"cmt">// y2 = class="num">11 + class="num">20  = class="num">32  像素
class=class="str">"cmt">//--- 创建
   if(!m_button1.Create(class="num">0,"Button1",class="num">0,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
   if(!m_button1.Text("Button1"))
      class="kw">return(class="kw">false);
   if(!AppWindow.Add(m_button1))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }

class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   Comment("");
class=class="str">"cmt">//--- 销毁对话框
   AppWindow.Destroy(reason);
  }

◍ 把图表事件转交给界面类

在 MT5 里给自定义面板加交互,第一步是接住图表事件。OnChartEvent 是终端在鼠标点击、键盘输入、对象拖拽时回调的入口,参数里 id 区分事件类型,lparam/dparam/sparam 携带具体数值或文本。 下面这段把事件直接转给 AppWindow 的 ChartEvent 方法,意味着所有按钮、编辑框的响应逻辑都收口到界面类里,主文件只做转发,不写业务。

MQL5 / C++
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam)class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 "Button1" 按钮                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
逐行看:第1行定义回调函数,四个入参对应终端事件结构;第6行只做一件事——把四个参数原样喂给 AppWindow.ChartEvent,由该类内部判断是点了 Button1 还是别的控件。 外汇与贵金属杠杆高、滑点随机,这类 UI 转发代码本身不预测行情,但能让你在实盘图上用按钮切换周期或手动平仓,减少手误。开 MT5 新建 EA,把这段粘进文件,再补一个带 Button1 的 AppWindow 类就能跑通点击事件。

MQL5 / C++
class="type">void OnChartEvent(const class="type">int id,        class=class="str">"cmt">// 事件 ID
                  const class="type">long& lparam,  class=class="str">"cmt">// 长整数类型的事件参数
                  const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                  const class="type">class="kw">string& sparam)class=class="str">"cmt">// 字符串类型的事件参数
  {
   AppWindow.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 "Button1" 按钮                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+

容器怎么带着整组控件一起挪

在 MT5 标准库里,AppWindow 面板本质是 CAppDialog 实例,它往上追溯是 CDialog,再往上是 CWndContainer。CWndContainer 正是那一组界面控件的共同基类,所以它天然有能力去统一调度容器内所有子控件的位移。 具体移动逻辑全压在 CWndContainer::Shift 里:先让父类 CWnd 做一次自身偏移,再遍历 m_controls 里的每个指针,逐个调用子控件的 Shift。下面这段是从标准库控件示例(Experts\MyExp\Help\With the Panel. EN\ControlsBmpButton.mq5 同系结构)抽出的核心方法,黄色与浅红高亮处就是循环搬移的关键。 想在 MT5 里验证,直接开个带面板的 EA,在拖拽窗口时打断点进 CWndContainer::Shift,能看到 total 等于你面板里挂的控件数,比如一个按钮加一个标签时 total=2,dx/dy 就是鼠标这次移动的像素差。外汇与贵金属图表上跑这类面板属于高频重绘,注意控件多了之后每帧循环开销会随控件数线性增长,可能拖慢 tick 处理。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 相对于控件组的移动                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWndContainer::Shift(const class="type">int dx,const class="type">int dy)
  {
class=class="str">"cmt">//--- 调用父类的方法
   if(!CWnd::Shift(dx,dy))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 在组内的元件中循环
   class="type">int total=m_controls.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CWnd *control=Control(i);
      class=class="str">"cmt">//--- 检查指针
      if(control==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- 移动组元素
      control.Shift(dx,dy);
     }
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }

「把按钮挂进对话框的客户区」

在 MT5 里直接全局声明 CButton 只是演示写法,真要写可维护面板,得让面板和按钮的创建逻辑都收进一个派生于 CAppDialog 的子类里。例子中 CAppWindowTwoButtons 就是这么做的,它把 Create、CreateButton1、CreateButton2 全封装在类内部,而不是散在全局作用域。 按钮对象刚 new 出来时,默认是独立控件,并不归面板管。关键一步是调用 CDialog::Add,把按钮指针塞进面板的客户区域,这样它才变成面板的依赖元件。 一旦 Add 成功,面板中心会把事件统一分发到各子控件,你不用自己写路由。下面这段取自 AppWindowTwoButtonsClass.mq5 的头部与类声明,定义了左边距 11 像素、顶距 11 像素、按钮宽 100 高 20、横向间距 5,这些数字直接决定你在图表上看到的面板布局。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     AppWindowTwoButtonsClass.mq5 |
class=class="str">"cmt">//|        Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                     [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.000"
class="macro">#class="kw">property description "控制面板与对话框. 演示 CButton 类"
class="macro">#include <Controls\Dialog.mqh>
class="macro">#include <Controls\Button.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 定义                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 缩进和距离
class="macro">#define INDENT_LEFT(class="num">11)      class=class="str">"cmt">// 与左边的缩进 (包括允许的边框宽度)
class="macro">#define INDENT_TOP(class="num">11)      class=class="str">"cmt">// 与顶部的缩进 (包括允许的边框宽度)
class="macro">#define CONTROLS_GAP_X(class="num">5)       class=class="str">"cmt">// X 坐标上的距离
class=class="str">"cmt">//--- 用于按钮
class="macro">#define BUTTON_WIDTH(class="num">100)     class=class="str">"cmt">//  X 坐标上的大小
class="macro">#define BUTTON_HEIGHT(class="num">20)      class=class="str">"cmt">// Y 坐标上的大小
class=class="str">"cmt">//---
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CAppWindowTwoButtons                                       |
class=class="str">"cmt">//| 使用: 控件应用程序的主对话框                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 双按钮对话框的类骨架怎么搭

在 MT5 自定义面板里放两个按钮,最省事的做法是继承标准库里的 CAppDialog,而不是从零画窗口。下面这段类声明把两个 CButton 成员和必要的创建接口都列出来了,直接抄进你的 EA 或脚本头文件就能用。 class CAppWindowTwoButtons : public CAppDialog { private: CButton m_button1; // 按钮对象 CButton m_button2; // 按钮对象 public: CAppWindowTwoButtons(void); ~CAppWindowTwoButtons(void); //--- 创建 virtual bool Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2); protected: //--- 创建依赖的控件 bool CreateButton1(void); bool CreateButton2(void); }; 逐行拆解:第 1 行让新类拥有对话框的基底能力;private 区的 m_button1、m_button2 是两个按钮实例,注释标明它们是按钮对象;public 区给了构造、析构和重写的 Create 方法,Create 的参数依次是图表 ID、对象名、子窗口号,以及两个按钮的对角坐标 (x1,y1) 与 (x2,y2),用像素定位;protected 区的 CreateButton1、CreateButton2 负责各自按钮的实例化,外部不直接调。 构造函数和析构函数体都是空的,说明资源交给标准库自动管理: CAppWindowTwoButtons::CAppWindowTwoButtons(void) { } CAppWindowTwoButtons::~CAppWindowTwoButtons(void) { } 开 MT5 新建一个 indicator 工程,把上面类声明贴进 .mqh,再在 Create 里补上两个按钮的调用,编译后拖到黄金 XAUUSD 图表上,就可能看到带双按钮的面板——外汇和贵金属波动大,面板只是辅助,实盘前先在策略测试器跑一遍。

MQL5 / C++
class CAppWindowTwoButtons : class="kw">public CAppDialog
  {
class="kw">private:
   CButton            m_button1;            class=class="str">"cmt">// 按钮对象
   CButton            m_button2;            class=class="str">"cmt">// 按钮对象
class="kw">public:
                     CAppWindowTwoButtons(class="type">void);
                    ~CAppWindowTwoButtons(class="type">void);
  class=class="str">"cmt">//--- 创建
  class="kw">virtual class="type">bool       Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2);
class="kw">protected:
  class=class="str">"cmt">//--- 创建依赖的控件
  class="type">bool               CreateButton1(class="type">void);
  class="type">bool               CreateButton2(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CAppWindowTwoButtons::CAppWindowTwoButtons(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
CAppWindowTwoButtons::~CAppWindowTwoButtons(class="type">void)
  {
  }

双按钮窗口的创建与生命周期挂钩

在 MT5 自定义界面开发里,把对话框对象做成全局实例是最直接的办法。下面这段把 CAppWindowTwoButtons 声明为 ExtDialog,意味着 EA 退出前它一直存活,初始化和反初始化直接操作同一个句柄。 Create() 先调父类 CAppDialog::Create 铺好坐标区域(左上 40,40 到右下 380,344,共 340×304 像素),再依次建两个按钮;任意一步失败就返回 false,调用方据此走 INIT_FAILED。这种链式创建顺序能避免控件悬空。 OnInit 里用 ExtDialog.Create(0,"含有两个按钮的 AppWindowClass",0,40,40,380,344) 把窗口挂到主图(subwin=0),成功后 Run() 进入消息循环;OnDeinitComment("") 清屏角标,再 Destroy(reason) 释放资源。外汇与贵金属 EA 挂界面时若忘了 Destroy,图表重载可能残留控件,属于高频踩坑点。 验证方式:把代码贴进 EA 模板,编译加载到 XAUUSD 的 M1 图,能看到固定位置浮出双按钮窗体;改 x2 从 380 到 280,窗口宽度会收缩 100 像素。

MQL5 / C++
class="type">bool CAppWindowTwoButtons::Create(const class="type">long chart,const class="type">class="kw">string name,const class="type">int subwin,const class="type">int x1,const class="type">int y1,const class="type">int x2,const class="type">int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 创建依赖的控件
   if(!CreateButton1())
      class="kw">return(class="kw">false);
   if(!CreateButton2())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 全局变量                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CAppWindowTwoButtons ExtDialog;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建应用程序对话框
   if(!ExtDialog.Create(class="num">0,"含有两个按钮的 AppWindowClass",class="num">0,class="num">40,class="num">40,class="num">380,class="num">344))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 运行应用程序
   ExtDialog.Run();
class=class="str">"cmt">//--- 成功
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易终止函数                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
   Comment("");
class=class="str">"cmt">//--- 销毁对话框
   ExtDialog.Destroy(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易图表事件函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+

「给自定义窗口挂上事件与双按钮」

在 MT5 里做带界面的指标或 EA,图表事件必须转发给对话框对象,否则按钮点不动。下面这段把 OnChartEvent 的四个参数原样丢给 ExtDialog.ChartEvent,窗口内的控件才能响应鼠标。 按钮的摆放完全靠像素算出来。Button1 左上角定在 (11,11),宽 100、高 20,所以右下角是 (111,32);INDENT_LEFT 和 INDENT_TOP 都是 11 像素的留白常量。 Button2 的 x1 不是顺手写的:它等于 11 + 2*(100+5) = 221 像素,其中 CONTROLS_GAP_X 是 5 像素的控件间距。也就是说两个按钮中间空出一段,不是紧挨着排。 CreateButton1 里连续三个 if 判断——创建、设文字、加入容器,任何一步失败就 return false。这种写法在 MT5 标准库控件里很常见,少一个 Add 调用按钮就不会显示在窗口上。外汇和贵金属图表加载这类自定义窗口存在脚本冲突导致卡图的高风险,建议在模拟盘先跑。

MQL5 / C++
class="type">void OnChartEvent(const class="type">int id,                     class=class="str">"cmt">// 事件 ID
                    const class="type">long& lparam,   class=class="str">"cmt">// 长整数类型的事件参数
                    const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度小数类型的事件参数
                    const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串类型的事件参数
  {
  ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 "Button1"                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CAppWindowTwoButtons::CreateButton1(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT;          class=class="str">"cmt">// x1       = class="num">11  像素
   class="type">int y1=INDENT_TOP;           class=class="str">"cmt">// y1       = class="num">11  像素
   class="type">int x2=x1+BUTTON_WIDTH;      class=class="str">"cmt">// x2 = class="num">11 + class="num">100 = class="num">111 像素
   class="type">int y2=y1+BUTTON_HEIGHT;     class=class="str">"cmt">// y2 = class="num">11 + class="num">20  = class="num">32  像素
class=class="str">"cmt">//--- 创建
   if(!m_button1.Create(class="num">0,"Button1",class="num">0,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
   if(!m_button1.Text("Button1"))
      class="kw">return(class="kw">false);
   if(!Add(m_button1))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 "Button2"                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CAppWindowTwoButtons::CreateButton2(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT+class="num">2*(BUTTON_WIDTH+CONTROLS_GAP_X);   class=class="str">"cmt">// x1 = class="num">11  + class="num">2 * (class="num">100 + class="num">5) = class="num">221 像素

◍ 按钮坐标与从属控件的落地写法

在 MT5 自定义面板里,第二个按钮的绘制区域由四个像素边界锁死:上边距 y1 固定为 11 像素,右边 x2 等于左边 x1(221)加按钮宽 100 得 321,下边 y2 等于 11 加高 20 得 31。这套硬编码数值直接决定了控件在对话框里的绝对位置,改一处就得同步算其余三处。 创建函数里先调 m_button2.Create(0,"Button2",0,x1,y1,x2,y2),任何一步返回 false 都直接退出,保证控件没挂上就不会继续跑后续逻辑。Text 和 Add 同样走了失败即返 false 的短路判断。 对话框总 Create 里先建自身,再依次 CreateButton1、CreateButton2,最后用 Add 把两个按钮收编进容器。外汇与贵金属图表上挂这类 EA 面板属于高风险操作,参数写错可能让面板不显示或拦截点击事件,建议在策略测试器里先跑一遍空图表验证坐标。

MQL5 / C++
  class="type">int y1=INDENT_TOP;                                                                     class=class="str">"cmt">// y1                    = class="num">11  像素
  class="type">int x2=x1+BUTTON_WIDTH;                                                                 class=class="str">"cmt">// x2 = class="num">221 + class="num">100        = class="num">321 像素
  class="type">int y2=y1+BUTTON_HEIGHT;                                                                class=class="str">"cmt">// y2 = class="num">11  + class="num">20         = class="num">31  像素
class=class="str">"cmt">//--- 创建
  if(!m_button2.Create(class="num">0,"Button2",class="num">0,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
  if(!m_button2.Text("Button2"))
      class="kw">return(class="kw">false);
  if(!Add(m_button2))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
  if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 创建依赖的控件
  if(!CreateButton1())
      class="kw">return(class="kw">false);
  if(!CreateButton2())
      class="kw">return(class="kw">false);
...
  if(!Add(m_button1))
      class="kw">return(class="kw">false);
...
  if(!Add(m_button2))
      class="kw">return(class="kw">false);
...

重写最小化逻辑避开单击交易面板

标准 CAppDialog 在最小化时默认把面板丢到 (10;10),会和 MT5 图表左上角的单击交易面板(One Click Trading)叠在一起,挡住下单按钮。要绕开这个坑,得在派生类里重写父类的 Minimize 方法,而不是改基类源码。 我们在 AppWindowCorrectMinimization.mq5 里基于带两个按钮的对话框demo做改动:先在 protected 区声明 virtual void Minimize(void),再写自己的实现。核心思路是用 ChartGetInteger 读 CHART_SHOW_ONE_CLICK 判断单击面板是否可见,visible 为 1 时把最小化顶部缩进设成 100 像素,不可见时只用 28 像素,这样最小化图标就不会压到交易面板。 代码里先取 m_min_rect 的上下边界算高度,若 top 不等于目标缩进就重设 top 并平移 bottom,最后才调 CAppDialog::Minimize() 完成基类动作。你在 MT5 里建个图表把单击交易面板打开,跑这个例子的面板最小化,就能看到图标自动掉到 y=100 而不是 28 的位置。外汇与贵金属杠杆品种价格波动剧烈,面板遮挡可能影响你手动平仓的响应速度,这类界面细节值得自己验证一遍。

MQL5 / C++
class="kw">protected:
  class=class="str">"cmt">//--- 创建依赖的控件
  class="type">bool              CreateButton1(class="type">void);
  class="type">bool              CreateButton2(class="type">void);
  class=class="str">"cmt">//--- 重写父方法
  class="kw">virtual class="type">void      Minimize(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CAppWindowCorrectMinimization::Minimize(class="type">void)
  {
class=class="str">"cmt">//--- 用于检查单击交易面板的变量l
  class="type">long one_click_visible=-class="num">1;  class=class="str">"cmt">// class="num">0 - 没有单击交易面板
  if(!ChartGetInteger(m_chart_id,CHART_SHOW_ONE_CLICK,class="num">0,one_click_visible))
    {
      class=class="str">"cmt">//--- 在专家日志中显示错误消息
      Print(__FUNCTION__+", 错误代码 = ",GetLastError());
    }
class=class="str">"cmt">//--- 用于最小化面板的最小缩进
  class="type">int min_y_indent=class="num">28;
  if(one_click_visible)
    min_y_indent=class="num">100;  class=class="str">"cmt">// 如果图表中有单击交易面板,就使用这个缩进
class=class="str">"cmt">//--- 取得最小化面板的当前缩进
  class="type">int current_y_top=m_min_rect.top;
  class="type">int current_y_bottom=m_min_rect.bottom;
  class="type">int height=current_y_bottom-current_y_top;
class=class="str">"cmt">//--- 计算应用程序中最小化面板从顶部的缩进
  if(m_min_rect.top!=min_y_indent)
    {
      m_min_rect.top=min_y_indent;
      class=class="str">"cmt">//--- 把下方边界移动到最小化图标之下
      m_min_rect.bottom=m_min_rect.top+height;
    }
class=class="str">"cmt">//--- 现在我们可以调用基类的方法了
  CAppDialog::Minimize();
  }

「用宏把 OnEvent 读成白话」

MT5 标准库 Controls 目录下的 Defines.mqh 里,有一组事件类型宏和事件处理图宏。前者把控件交互翻译成整数代号,后者让 CAppDialog::OnEvent 不用写一长串 if-else,而是像查表一样把事件映射到具体方法。 下面这段就是事件类型宏的原貌,从点击到拖曳一共 15 个基础事件,外加代号 100 的应用关闭事件: #define ON_CLICK (0) // 点击控件的事件 #define ON_DBL_CLICK (1) // 双击控件的事件 #define ON_SHOW (2) // 显示控件的事件 #define ON_HIDE (3) // 隐藏控件的事件 #define ON_CHANGE (4) // 修改控件的事件 #define ON_START_EDIT (5) // 开始编辑的事件 #define ON_END_EDIT (6) // 编辑结束的事件 #define ON_SCROLL_INC (7) // 滚动条增加的事件 #define ON_SCROLL_DEC (8) // 滚动条减小的事件 #define ON_MOUSE_FOCUS_SET (9) // 鼠标光标进入控件的事件 #define ON_MOUSE_FOCUS_KILL (10) // 鼠标光标退出控件的事件 #define ON_DRAG_START (11) // 开始拖曳控件的事件 #define ON_DRAG_PROCESS (12) // 正在拖曳控件的事件 #define ON_DRAG_END (13) // 结束拖曳控件的事件 #define ON_BRING_TO_TOP (14) // 鼠标事件优先级提高的事件 #define ON_APP_CLOSE (100) // 关闭应用程序的事件 把宏套进 OnEvent 后,代码可读性立刻不一样。比如收到 m_bmpbutton1 的自定义点击(ON_CLICK),就直接调 OnClickBmpButton1();m_bmpbutton2 同理调 OnClickBmpButton2(),逻辑一眼能看完。 实际落地时,以 AppWindowTwoButtonsClass.mq5 为底改一个 Events 版本:声明 OnEvent、OnClickButton1、OnClickButton2,按钮一文字改成开仓、按钮二改成清仓。面板 protected 区挂上 CTrade、CPositionInfo、CAccountInfo 实例,点击方法里分别发买入和处理平仓。外汇贵金属杠杆高,模拟账户先跑通再上真钱,别直接拿去实盘押注。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define ON_CLICK(class="num">0)   class=class="str">"cmt">// 点击控件的事件
class="macro">#define ON_DBL_CLICK(class="num">1)   class=class="str">"cmt">// 双击控件的事件
class="macro">#define ON_SHOW(class="num">2)   class=class="str">"cmt">// 显示控件的事件
class="macro">#define ON_HIDE(class="num">3)   class=class="str">"cmt">// 隐藏控件的事件
class="macro">#define ON_CHANGE(class="num">4)   class=class="str">"cmt">// 修改控件的事件
class="macro">#define ON_START_EDIT(class="num">5)   class=class="str">"cmt">// 开始编辑的事件
class="macro">#define ON_END_EDIT(class="num">6)   class=class="str">"cmt">// 编辑结束的事件
class="macro">#define ON_SCROLL_INC(class="num">7)   class=class="str">"cmt">// 滚动条增加的事件
class="macro">#define ON_SCROLL_DEC(class="num">8)   class=class="str">"cmt">// 滚动条减小的事件
class="macro">#define ON_MOUSE_FOCUS_SET(class="num">9)   class=class="str">"cmt">//  "鼠标光标进入控件" 的事件
class="macro">#define ON_MOUSE_FOCUS_KILL(class="num">10)  class=class="str">"cmt">// "鼠标光标退出控件"的事件
class="macro">#define ON_DRAG_START(class="num">11)  class=class="str">"cmt">// "开始拖曳控件"的事件
class="macro">#define ON_DRAG_PROCESS(class="num">12)  class=class="str">"cmt">// "正在拖曳控件"的事件
class="macro">#define ON_DRAG_END(class="num">13)  class=class="str">"cmt">// "结束拖曳控件"的事件
class="macro">#define ON_BRING_TO_TOP(class="num">14)  class=class="str">"cmt">// "鼠标事件优先级提高"的事件
class="macro">#define ON_APP_CLOSE(class="num">100) class=class="str">"cmt">// "关闭应用程序"的事件
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用宏把图表事件映射写短

在 MT5 自定义面板里,图表事件靠 OnEvent 分发,但原生写法要把 id、lparam、dparam、sparam 逐个比,代码又长又易错。下面这组宏把常用匹配逻辑压成一行,直接声明「什么事件+哪个控件+调哪个函数」即可。 INTERNAL_EVENT 定义为 -1,用来标记内部事件;EVENT_MAP_BEGIN 展开成 OnEvent 函数头,EVENT_MAP_END 则把未命中的事件抛给父类处理。这样继承 CAppDialog 的子类只需填中间几行。 实际挂两个位图按钮就这么写:ON_EVENT(ON_CLICK,m_bmpbutton1,OnClickBmpButton1) 表示自定义事件 ON_CLICK 且 lparam 等于按钮 Id 时,执行对应点击函数并返回 true。回测面板响应延迟通常在 1~2 个 tick 内,比手写 if 链清晰,也方便后续加 m_bmpbutton3 不改结构。外汇与贵金属图表插件改动需自测,GUI 逻辑异常可能导致下单面板卡死,属高风险操作。

MQL5 / C++
class="macro">#define INTERNAL_EVENT(-class="num">1)
class="macro">#define EVENT_MAP_BEGIN(class_name)     class="type">bool class_name::OnEvent(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="macro">#define EVENT_MAP_END(parent_class_name)     class="kw">return(parent_class_name::OnEvent(id,lparam,dparam,sparam)); }
class="macro">#define ON_EVENT(event,control,handler)       if(id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) { handler(); class="kw">return(true); }
class="macro">#define ON_EVENT_PTR(event,control,handler)    if(control!=NULL && id==(event+CHARTEVENT_CUSTOM) && lparam==control.Id()) { handler(); class="kw">return(true); }
class="macro">#define ON_NO_ID_EVENT(event,handler)          if(id==(event+CHARTEVENT_CUSTOM)) { class="kw">return(handler()); }
class="macro">#define ON_NAMED_EVENT(event,control,handler)  if(id==(event+CHARTEVENT_CUSTOM) && sparam==control.Name()) { handler(); class="kw">return(true); }
class="macro">#define ON_INDEXED_EVENT(event,controls,handler) { class="type">int total=ArraySize(controls); for(class="type">int i=class="num">0;i<total;i++) if(id==(event+CHARTEVENT_CUSTOM) && lparam==controls[i].Id()) class="kw">return(handler(i)); }
class="macro">#define ON_EXTERNAL_EVENT(event,handler)       if(id==(event+CHARTEVENT_CUSTOM)) { handler(lparam,dparam,sparam); class="kw">return(true); }
EVENT_MAP_BEGIN(CControlsDialog)
ON_EVENT(ON_CLICK,m_bmpbutton1,OnClickBmpButton1)
ON_EVENT(ON_CLICK,m_bmpbutton2,OnClickBmpButton2)
EVENT_MAP_END(CAppDialog)
class="type">bool CControlsDialog::OnEvent(const class="type">int id,const class="type">long& lparam,const class="type">class="kw">double& dparam,const class="type">class="kw">string& sparam) {

给双按钮面板接上点击事件

在 MT5 自定义面板里,按钮不是画上去就完事,得在 OnEvent 里把图表自定义事件和具体控件 ID 对上号,否则点下去毫无反应。 下面这段是 CControlsDialog 里处理两个位图按钮点击的典型写法:先判断事件是不是 ON_CLICK+CHARTEVENT_CUSTOM,再用 lparam 比对 m_bmpbutton1.Id() 和 m_bmpbutton2.Id(),命中就调各自回调函数并返回 true,没命中才丢给父类 CAppDialog::OnEvent。

MQL5 / C++
if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton1.Id()) { OnClickBmpButton1(); class="kw">return(true); }
if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton2.Id()) { OnClickBmpButton2(); class="kw">return(true); }
class="kw">return(CAppDialog::OnEvent(id,lparam,dparam,sparam)); }
class="type">bool CControlsDialog::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
   if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton1.Id())
     {
      OnClickBmpButton1();
      class="kw">return(true);
     }
   if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton2.Id())
     {
      OnClickBmpButton2();
      class="kw">return(true);
     }
   class="kw">return(CAppDialog::OnEvent(id,lparam,dparam,sparam));
  }
逐行拆解:第 1 行用 ON_CLICK+CHARTEVENT_CUSTOM 过滤出按钮点击类自定义事件;lparam==m_bmpbutton1.Id() 是精确定位第一个按钮;OnClickBmpButton1() 执行你的逻辑(比如切换周期或发通知);return(true) 表示事件已消费,阻止冒泡。第二组和第一组对称,只是换了第二个按钮。最后一行把剩余事件交回基类,保证窗口拖拽等默认行为不失效。 实际写 CAppWindowTwoButtons 时,类里要声明两个 CButton 成员 m_button1、m_button2,并暴露 OnClickButton1 / OnClickButton2 作为保护级回调。外汇与贵金属行情波动剧烈、杠杆风险高,这类面板只做辅助触发,别把它当成进出场唯一依据;开 MT5 挂上这段,点按钮看 Experts 日志有没有进对应的函数,就能验证事件链路通不通。

MQL5 / C++
if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton1.Id()) { OnClickBmpButton1(); class="kw">return(true); }
if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton2.Id()) { OnClickBmpButton2(); class="kw">return(true); }
class="kw">return(CAppDialog::OnEvent(id,lparam,dparam,sparam)); }
class="type">bool CControlsDialog::OnEvent(const class="type">int id,const class="type">long &lparam,const class="type">class="kw">double &dparam,const class="type">class="kw">string &sparam)
  {
   if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton1.Id())
     {
      OnClickBmpButton1();
      class="kw">return(true);
     }
   if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_bmpbutton2.Id())
     {
      OnClickBmpButton2();
      class="kw">return(true);
     }
   class="kw">return(CAppDialog::OnEvent(id,lparam,dparam,sparam));
  }

「给双按钮面板接上交易动作」

在 MT5 自定义面板里挂两个按钮不难,难的是让点击真正落到账户上。下面这段类声明把仓位、交易、账户三个标准库对象塞进对话框子类,点击按钮才有东西可操作。

MQL5 / C++
class CAppWindowTwoButtons : class="kw">public CAppDialog
{
class="kw">protected:
  CPositionInfo    m_position;   class=class="str">"cmt">// 交易仓位对象
  CTrade           m_trade;      class=class="str">"cmt">// 交易对象
  CAccountInfo     m_account;    class=class="str">"cmt">// 账户信息封装
class="kw">private:
  CButton          m_button1;    class=class="str">"cmt">// 按钮对象
  CButton          m_button2;    class=class="str">"cmt">// 按钮对象
  class="type">bool             CreateButton1(class="type">void);
  class="type">bool             CreateButton2(class="type">void);
  class="type">void             OnClickButton1(class="type">void);
  class="type">void             OnClickButton2(class="type">void);
};
事件映射把按钮的 ON_CLICK 绑定到各自处理函数,这是 GUI 不卡死的关键:
MQL5 / C++
EVENT_MAP_BEGIN(CAppWindowTwoButtons)
ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
EVENT_MAP_END(CAppDialog)
构造函数里给按钮写死文字,m_button1 标 "Open BUY"、m_button2 标 "Close",任一处 Text() 返回 false 就直接 return(false),面板创建会失败。 点击逻辑只对模拟账户放行:m_account.TradeMode() 等于 ACCOUNT_TRADE_MODE_DEMO 时才 m_trade.Buy(1.0)。实盘把这段判断去掉前,先想清楚 1.0 手在贵金属上可能瞬间吃掉可观保证金,外汇和贵金属都是高风险品种。

MQL5 / C++
class CAppWindowTwoButtons : class="kw">public CAppDialog
{
class="kw">protected:
  CPositionInfo    m_position;   class=class="str">"cmt">// 交易仓位对象
  CTrade           m_trade;      class=class="str">"cmt">// 交易对象
  CAccountInfo     m_account;    class=class="str">"cmt">// 账户信息封装
class="kw">private:
  CButton          m_button1;    class=class="str">"cmt">// 按钮对象
  CButton          m_button2;    class=class="str">"cmt">// 按钮对象
  class="type">bool             CreateButton1(class="type">void);
  class="type">bool             CreateButton2(class="type">void);
  class="type">void             OnClickButton1(class="type">void);
  class="type">void             OnClickButton2(class="type">void);
};

EVENT_MAP_BEGIN(CAppWindowTwoButtons)
ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
EVENT_MAP_END(CAppDialog)

class="type">void CAppWindowTwoButtons::OnClickButton1(class="type">void)
  {
  if(m_account.TradeMode()==ACCOUNT_TRADE_MODE_DEMO)
     m_trade.Buy(class="num">1.0);
  }

◍ demo 账户里一键平掉当前品种仓位

在自定义面板里给第二个按钮挂上点击事件,最实用的场景就是快速清仓。下面这段代码只针对 demo 账户生效,实盘不会被误触,对外汇和贵金属这类高波动品种来说,这种隔离能避免手滑。 逻辑是先拿 AccountTradeMode 判断是不是 ACCOUNT_TRADE_MODE_DEMO,是才进循环。PositionsTotal 返回当前挂着的仓位总数,从最后一个索引往前扫,能避开删除元素导致的越界。 每找到一个仓位用 SelectByIndex 按索引选中,再比对我们当前图表品种 Symbol,一致就调 PositionClose 传 ticket 平掉。整套动作在 MT5 里点一下按钮就能跑,你可以把这段直接塞进自己的 CAppWindowTwoButtons 类里验证。

MQL5 / C++
class="type">void CAppWindowTwoButtons::OnClickButton2(class="type">void)
  {
   if(m_account.TradeMode()==ACCOUNT_TRADE_MODE_DEMO)
      for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--) class=class="str">"cmt">// 返回当前仓位的数量
         if(m_position.SelectByIndex(i)) class=class="str">"cmt">// 根据索引选择仓位,以进一步访问它的属性
            if(m_position.Symbol()==Symbol())
               m_trade.PositionClose(m_position.Ticket()); class=class="str">"cmt">// 根据指定的交易品种关闭仓位
  }

记住这一条就够了

自己写 MT5 面板时,最省事的起点就是 CAppDialog:它是标准库里图形面板的基类,内部控件的属性都能通过继承结构直接改。配套示例文件里 LearnCAppDialog.mq5 是最少代码骨架,AppWindowEditDefine.mq5 则演示了重定义控件常数的非标准手法。 高 DPI 屏上面板“浮动”的根因很实在:Controls/Defines.mqh 里所有布局常量都是写死的像素值,换缩放比就错位。用 #undef 把原常数清掉再 #define 新值,能快速给自己修好显示,但位图按钮图标仍依赖 res 目录里的 bmp,放大必糊。 外汇与贵金属图表挂 EA 面板属高风险操作,改界面常数不影响订单逻辑,但误删对象可能引发运行时报错。开 MT5 把下面这段代码塞进 EA 头文件,编译后拖到 XAUUSD 图表看标题栏是否被拉高到 60 像素,就能验证重定义是否生效。

MQL5 / C++
class="macro">#include <Controls\Defines.mqh>
class=class="str">"cmt">// 删除旧值
class="macro">#undef  CONTROLS_DIALOG_CAPTION_HEIGHT
class="macro">#undef  CONTROLS_DIALOG_BUTTON_OFF
class="macro">#undef  CONTROLS_BUTTON_SIZE
class=class="str">"cmt">// 重新定义新值
class="macro">#define CONTROLS_DIALOG_CAPTION_HEIGHT   class="num">60  class=class="str">"cmt">// 窗口标题高度
class="macro">#define CONTROLS_DIALOG_BUTTON_OFF       class="num">20  class=class="str">"cmt">// 按钮缩进/位置(交叉、折叠)
class=class="str">"cmt">//--- BmpButton
class="macro">#define CONTROLS_BUTTON_SIZE             class="num">40  class=class="str">"cmt">// 按钮的默认大小(class="num">16 x class="num">16)
交给小布盯盘看盘口
这些面板结构和事件宏的排查,小布盯盘的 AIGC 已内置诊断,打开对应品种页即可看到控件层级提示,把重复劳动交给小布,你专注决策。

常见问题

若未在程序退出或重置时调用 Destroy 方法销毁面板,改变时段或品种会在原控件上再建新层,必须在 OnDeinit 里显式销毁。
小布盯盘的 AIGC 模块可基于你描述的控件需求输出结构参考,但具体事件重写和常数重定义仍需在 MT5 里实测校验。
通过继承标准控件类并重写其事件处理方法,可改变默认响应逻辑,文中给出了重写标准控件行为的完整路径。
用来解除标准库已定义的控件常数,便于在同文件内按自己的布局重新赋值,避免编译期常数冲突。
由对象在控件组中的添加次序与父子继承关系共同决定,后加入的通常绘制在上层,移动时也沿继承链传递。