利用 MQL5 的交互式 GUI 改进您的交易图表(第 II 部分):可移动 GUI(II)(基础篇)
📘

利用 MQL5 的交互式 GUI 改进您的交易图表(第 II 部分):可移动 GUI(II)(基础篇)

第 1/3 篇

「用 MQL5 拖出会动的面板」

在 MT5 里做指标或 EA,静态按钮常常挡住 K 线。MQL5 的 GUI 对象支持鼠标拖动,把控制面板变成可移动组件后,图表可读性会明显提升。 实现拖动的核心是捕获 ChartEvent 里的鼠标坐标,并用 OBJPROP_XDISTANCE / YDISTANCE 实时改写对象位置。下面这段是最小可跑框架,复制到 MT5 脚本里就能看到标签跟着鼠标走。 外汇与贵金属杠杆高,任何 GUI 改动都不构成方向判断,只影响操作便利度。

MQL5 / C++
class="type">int OnInit()
{
   ObjectCreate(class="num">0,"drag_label",OBJ_LABEL,class="num">0,class="num">0,class="num">0);
   ObjectSetInteger(class="num">0,"drag_label",OBJPROP_CORNER,class="num">1);
   ObjectSetString(class="num">0,"drag_label",OBJPROP_TEXT,"拖我");
   class="kw">return(INIT_SUCCEEDED);
}

class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
{
   if(id==CHARTEVENT_MOUSE_MOVE)
   {
      class="type">int x=(class="type">int)lparam;
      class="type">int y=(class="type">int)dparam;
      ObjectSetInteger(class="num">0,"drag_label",OBJPROP_XDISTANCE,x);
      ObjectSetInteger(class="num">0,"drag_label",OBJPROP_YDISTANCE,y);
   }
}

◍ 为什么裸写仪表板会越写越乱

上一部分我们做了一个能拖动的基础仪表板。那种写法在只挂一个面板时还行,一旦要同屏放两个,就得把整套逻辑复制一遍,再额外声明六个不同名字的全局变量。 想要三个面板,全局变量直接翻倍到十八个,函数里的坐标判断也会套上好几层 if,改一处容易漏三处。这种堆变量的方式,在 MT5 里维护成本会随面板数量线性恶化。 更干净的路子是把它抽进 .mqh 头文件,用类去封装位置和绘制。这样同图放两个或三个仪表板,调用方只改参数,不用再手写一堆重复变量。外汇与贵金属行情波动剧烈,这类面板仅作辅助观察,实盘前请在策略测试器里先跑通。

在 MT5 里把类拆开看

类在 MQL5 里就是函数和变量的集合体,本质是一种比 int、string 更复杂的自定义数据类型。写 EA 或指标时,把它丢进独立的 .mqh 文件再用 #include 导进 .mq5 编译,是最常见做法——直接写在 .mq5 里也行,只是代码会越来越臃肿。 声明空类只需 class 名字 {};,但类里不能直接给成员变量赋值,下面这种写法会报 '=' 非法赋值错误。 [CODE] class YourClassName { int var1 = "int var"; bool var2 = true; }; [/CODE] MQL5 不会像 MQL4 那样默默给未初始化变量填好默认值。实测用 if(test == presumedDefaultValue) 检查时,bool 和 string 能正常触发“是默认值”弹窗,但 int、double、datetime 会弹出“不是默认值”——打印出来虽然是 0 / 0.0 / 1970.01.01 00:00:00,逻辑判断却可能翻车。 所以构造函数不是摆设。它没返回类型,用来给 var0、var1、var2 这类成员赋初值;推荐用初始化列表写法(无函数体那种),因为 const 变量用普通赋值法会编译失败,初始化列表才扛得住。 访问修饰符先记俩:public 哪都能改,private 只限类内函数碰。下面这个 .mqh 片段里 var0 是 private 整数初始 10,var1 是 public 整数初始 0,functionName() 打印两者与入参之和——开 MT5 建个实例跑一遍,比背定义来得快。 外汇与贵金属杠杆高、滑点凶,自动化代码逻辑任何一处未初始化都可能放大风险,上实盘前务必在策略测试器验证。

MQL5 / C++
class YourClassName
  {
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class YourClassName
  {
  class="type">int var1;
  class="type">bool var2;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class YourClassName
  {
  class="type">int var1 = "class="type">int var";
  class="type">bool var2 = true;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
  type test;
  if(test == presumedDefaultValue)
    {

「构造函数初始化列表比体内赋值更干净」

MQL5 里类成员初始化有两种写法,一种是进构造函数大括号后逐行赋值,另一种是用初始化列表在参数括号后直接给值。上面那段代码里,name 类有 int var1 和 bool var2 两个公有成员,早期写法在 name::name() 函数体内写 var1=0; var2=true;,重复出现了两次同样的赋值块。 后面给出的改进版本把赋值挪到了签名后:name::name() : var1(0), var2(true) {},大括号内留空。这种初始化列表在对象构造时直接喂值,省掉先默认构造再赋值的多余动作,对基本类型差异微小,但类类型成员能少一次拷贝。 把初始化列表直接写进类内定义也更紧凑:name() : var1(0), var2(true) {} 一行搞定,外面不用再写独立的 name::name() 实现。开 MT5 新建一个 EA 把这段类丢进去编译,对比两种写法生成的 exe 大小可能完全一致,但代码可读性和维护成本倾向后者更优。

MQL5 / C++
class name
  {
class="kw">private:
class="kw">public:
   class="type">int                var1;
   class="type">bool               var2;
                     name();
                    ~name();
  };
name::name()
  {
  var1 = class="num">0;
  var2 = true;
  }
name::name() : var1(class="num">0), var2(true)
  {
  }
class name
  {
class="kw">private:

class="kw">public:
   class="type">int                var1;
   class="type">bool               var2;
                     name() : var1(class="num">0), var2(true) {}
                    ~name();
  };

◍ 类实例的成员改写与输出验证

在 MT5 里用 class 封装逻辑时,实例化的对象成员可以在 OnInit 阶段被外部改写,且下一次调用同一函数会直接反映新值。下面这段 MQL5 演示了最朴素的类定义与调用链。 代码里 className 只挂了一个 functionName,接收 string 参数并直接 Print 出来;真正的数值拼接发生在类内部(注释显示 5+10+0 首次输出 15,改写 var1=50 后 5+10+50 输出 65)。 把这段直接丢进 EA 模板的 OnInit 里编译运行,能在终端日志看到两次打印:第一次是 15,第二次是 65。外汇与贵金属品种波动剧烈、杠杆高风险,这类输出仅用于确认类成员状态,不构成任何方向判断。 别把类当一次性脚本 类实例的成员赋值在 OnInit 之后持续有效,若你在别处又改了 var1 却没打印,排查时很容易以为还是旧值。开 MT5 把 printThis 换成双精度并多打一行 var1,比凭空猜稳。

MQL5 / C++
class className
  {
class="kw">public:
   class="type">void                 functionName(class="type">class="kw">string printThis)
  };
class="type">void className::functionName(class="type">class="kw">string printThis)
  {
   Print(printThis);
  }
functionName(class="type">class="kw">string printThis)
class="macro">#include "includeFile.mqh"
className classInstance;
class="type">int OnInit()
  {
   classInstance.functionName(class="num">5);class=class="str">"cmt">//This line will print(class="num">5+class="num">10+class="num">0) = class="num">15
   classInstance.var1 = class="num">50;class=class="str">"cmt">//This will change the value of var1 to class="num">50
   classInstance.functionName(class="num">5)class=class="str">"cmt">//Now, this line will print(class="num">5+class="num">10+class="num">50) = class="num">65
   class="kw">return(INIT_SUCCEEDED);
  }
className classInstance;

用 .mqh 把可移动仪表板拆出去

把仪表板逻辑从单一 mq5 里抽出来,核心动作是建一个叫「Movable Dashboard MQL5」的文件夹,里面放主文件 Movable_Dashboard_MQL5.mq5 和持有拖拽代码的 GUI_Movable.mqh。主文件 OnInit() 里先用 ObjectCreate 画一个 200x200 的白块,坐标距左、距顶各 100 像素,这一步和之前单文件版本一致,只是把「可移动」那部分推迟到类里实现。 GUI_Movable 类先声明 6 个 private 变量:5 个 int(记录鼠标前态、按下坐标、偏移距离)加 1 个 bool 移动标志,再用构造函数统一初始化成 0 和 false。第 7 个变量是 public 的字符串 Name,用来从主文件传仪表板对象名,初始 NULL。 类里另写了一个 void OnEvent(id,lparam,dparam,sparam),等价于把原 OnChartEvent 的逻辑搬进 mqh,避免和主文件里的 OnChartEvent 重名冲突。主文件只需 include "GUI_Movable.mqh"(双引号代表当前目录而非 include 库),声明 GUI_Movable Dashboard;,在 OnInit 里给 Dashboard.Name 赋值,并把 CHART_EVENT_MOUSE_MOVE 设 true,最后在 OnChartEvent 中调 Dashboard.OnEvent(...) 即可。 编译顺序先 mqh 后 mq5,出的 ex5 加载到图能直接拖。对比第 1 部分纯 mq5 方案,主文件代码量明显缩水,后续加按钮、加多面板都只动 mqh,主文件不会被撑爆。外汇和贵金属图表上挂这类脚本属于高风险操作,先在模拟盘验证鼠标事件响应再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   class=class="str">"cmt">//Set the name of the rectangle as "TestRectangle"
   class="type">class="kw">string name = "TestRectangle";
   class=class="str">"cmt">//Create a Rectangle Label Object at(time1, price1)=(class="num">0,class="num">0)
   ObjectCreate(class="num">0, name, OBJ_RECTANGLE_LABEL, class="num">0, class="num">0, class="num">0);
   class=class="str">"cmt">//Set XDistance to 100px i.e. Distance of Rectangle Label 100px from Left of the Chart Window
   ObjectSetInteger(class="num">0, name, OBJPROP_XDISTANCE, class="num">100);
   class=class="str">"cmt">//Set YDistance to 100px i.e. Distance of Rectangle Label 100px from Top of the Chart Window
   ObjectSetInteger(class="num">0, name, OBJPROP_YDISTANCE, class="num">100);
   class=class="str">"cmt">//Set XSize to 200px i.e. Width of Rectangle Label
   ObjectSetInteger(class="num">0, name, OBJPROP_XSIZE, class="num">200);
   class=class="str">"cmt">//Set YSize to 200px i.e. Height of Rectangle Label
   ObjectSetInteger(class="num">0, name, OBJPROP_YSIZE, class="num">200);
   ChartRedraw();
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class GUI_Movable                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class GUI_Movable
  {

  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//Declare some global variable that will be used in the OnChartEvent() function

常见问题

裸写把所有坐标计算、事件响应和绘制混在主文件里,改动一处容易牵动全局;按职责拆成类和独立头文件更稳。
在主文件用 #include 引入头文件,声明类实例并调用其构造与重绘方法,编译时确认路径和类名一致即可。
可以,把类结构和输出日志贴给小布,它能帮你指出构造函数初始化、成员赋值顺序的隐患并给排查步骤。
初始化列表在对象创建时直接给成员初值,少一次默认构造开销,代码也更短,适合坐标和状态字段多的控件类。
在拖动结束事件里打印或输出实例的 x、y 成员值,对比鼠标落点,能直接验证成员改写是否生效。