利用 MQL5 的交互式 GUI 改进您的交易图表(第 II 部分):可移动 GUI(II)(基础篇)
「用 MQL5 拖出会动的面板」
在 MT5 里做指标或 EA,静态按钮常常挡住 K 线。MQL5 的 GUI 对象支持鼠标拖动,把控制面板变成可移动组件后,图表可读性会明显提升。 实现拖动的核心是捕获 ChartEvent 里的鼠标坐标,并用 OBJPROP_XDISTANCE / YDISTANCE 实时改写对象位置。下面这段是最小可跑框架,复制到 MT5 脚本里就能看到标签跟着鼠标走。 外汇与贵金属杠杆高,任何 GUI 改动都不构成方向判断,只影响操作便利度。
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 建个实例跑一遍,比背定义来得快。
外汇与贵金属杠杆高、滑点凶,自动化代码逻辑任何一处未初始化都可能放大风险,上实盘前务必在策略测试器验证。
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 大小可能完全一致,但代码可读性和维护成本倾向后者更优。
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,比凭空猜稳。
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,主文件不会被撑爆。外汇和贵金属图表上挂这类脚本属于高风险操作,先在模拟盘验证鼠标事件响应再上实盘。
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