利用 EX5 库来推动您的项目开发·进阶篇
📦

利用 EX5 库来推动您的项目开发·进阶篇

(2/3)· 直接导出类在 MQL5 仍受限,用虚函数与多态把实施藏进 ex5 才是多人协作的正路

偏理论 第 2/3 篇
把核心算法塞进 mqh 发给合作方,等于把源码白送人。用 ex5 库封装实施细节,才能在共享与保护之间留出口。本文接着第 1 篇,把类级隐藏与继承的坑一次讲清。

◍ 把类藏进 ex5:用虚函数拆出公开接口

MQL5 至今不支持把类直接 export 给别的 ex5 模块,想跟别人共享 CSpiro 这种绘图类又不暴露算法,只能玩拆类。思路是把一个类劈成两半:声明和虚函数留在公开头文件里,真实实现塞进带 #property library 的隐藏 mq5。 具体做法是拷贝一份 CSpiro 的后代叫 ISpiro(I 取自 interface),原类只留变量和 virtual 声明的空壳,ISpiro 才写具体逻辑。再导出一个工厂函数 iSpiro() 返回 new ISpiro,外部拿到的是基类指针,看不见底层。 注意所有要共享的函数必须带 virtual 前缀,否则多态不生效。还有 SetPoint 在 Graph 库和 CSpiro 里都声明了,隐藏文件里用 :: 加文件名限定,编译器才知道调哪一个。 下面这段是公开头加隐藏实现的精简例证,复制进 MT5 建两个文件就能编译出只暴露接口的库。外汇贵金属自动化用这类封装时记住:封装不等于低风险,策略逻辑黑盒化后回测偏差可能更大。

MQL5 / C++
class=class="str">"cmt">//--- Spiro.mqh – class="kw">public file, the so called header file
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CSpiro                                                     |
class=class="str">"cmt">//| Spirograph draw class                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSpiro
  {
class="kw">public:
  class=class="str">"cmt">//--- prefix of the chart objects
  class="type">class="kw">string            m_sID;
  class=class="str">"cmt">//--- offset of the chart center
  class="type">int               m_x0,m_y0;
  class=class="str">"cmt">//--- class="type">color of the line
  class="type">color             m_clr;
  class=class="str">"cmt">//--- chart parameters
  class="type">class="kw">double            m_R,m_r,m_D,m_dAlfa,m_nRotate;
class="kw">public:
  class=class="str">"cmt">//--- constructor
                     CSpiro() { };
  class=class="str">"cmt">//--- destructor
                    ~CSpiro() { };
  class="kw">virtual class="type">void Init(class="type">int ax0,class="type">int ay0,class="type">color aclr,class="type">class="kw">string asID) { };
  class="kw">virtual class="type">void SetData(class="type">class="kw">double aR,class="type">class="kw">double ar,class="type">class="kw">double aD,class="type">class="kw">double adAlpha,class="type">class="kw">double anRotate) { };
class="kw">public:
  class="kw">virtual class="type">void DrawSpiro() { };
  class="kw">virtual class="type">void SetPoint(class="type">int x,class="type">int y) { };
  };
class=class="str">"cmt">//--- ISpiro.mq5 – hidden implementation file
class="macro">#include "Spiro.mqh"
class=class="str">"cmt">//--- importing some functions
class="macro">#class="kw">import "..\Experts\Spiro\Graph.ex5"
class="type">void SetPoint(class="type">long achart,class="type">class="kw">string name,class="type">int awnd,class="type">int ax,class="type">int ay,class="type">color aclr);
class="type">void ObjectsDeleteAll2(class="type">long achart=class="num">0,class="type">int wnd=-class="num">1,class="type">int type=-class="num">1,class="type">class="kw">string pref="",class="type">class="kw">string excl="");
class="macro">#class="kw">import
CSpiro *iSpiro() class="kw">export { class="kw">return(new ISpiro); }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Сlass ISpiro                                                     |
class=class="str">"cmt">//| Spirograph draw class                                            |

用类封装把螺线画进图表

把万花尺(Spirograph)曲线搬进 MT5,核心是先定义一个继承自 CSpiro 的子类 ISpiro,把坐标原点、颜色、对象前缀等状态收进成员,避免每次重绘都从头传参。 构造函数里把 m_x0、m_y0 置 0,析构时调 ObjectsDeleteAll(0,0,-1) 清掉当前图表全部对象;Init() 接收 ax0/ay0/aclr/asID 四个参数,把绘图基准和命名空间定下来,同时把 m_R、m_r、m_D 先归零,等 SetData() 再灌真实几何参数。 SetData() 一口气写入大圆半径 m_R、小圆半径 m_r、笔尖偏移 m_D、步长 m_dAlfa 和旋转圈数 m_nRotate。DrawSpiro() 先卡三道防线:r、D、Alpha 任一为 0 就 Print 报错退出,防止除零或死循环。 真正跑循环时,以 m_dAlfa 为步长从 0 累加到 m_nRotate*2*3.1415926(即圈数乘 2π),每步用 (m_R-m_r)*cos(a)+m_D*cos((m_R-m_r)/m_r*a) 算 x、用对应 sin 算 y,再交给 SetPoint() 落点。外汇与贵金属图表叠加这类几何图形属高风险辅助观察,参数错配可能画出无关噪声,建议先在策略测试器用纯数值验证。

MQL5 / C++
class ISpiro : class="kw">public CSpiro
  {
class="kw">public:
                     ISpiro() { m_x0=class="num">0; m_y0=class="num">0; };
                    ~ISpiro() { ObjectsDeleteAll(class="num">0,class="num">0,-class="num">1); };
  class="kw">virtual class="type">void      Init(class="type">int ax0,class="type">int ay0,class="type">color aclr,class="type">class="kw">string asID);
  class="kw">virtual class="type">void      SetData(class="type">class="kw">double aR,class="type">class="kw">double ar,class="type">class="kw">double aD,class="type">class="kw">double adAlpha,class="type">class="kw">double anRotate);
class="kw">public:
  class="kw">virtual class="type">void      DrawSpiro();
  class="kw">virtual class="type">void      SetPoint(class="type">int x,class="type">int y);
  };
class="type">void ISpiro::Init(class="type">int ax0,class="type">int ay0,class="type">color aclr,class="type">class="kw">string asID)
  {
   m_x0=ax0;
m_y0=ay0;
m_clr=aclr;
m_sID=asID;
   m_R=class="num">0;
m_r=class="num">0;
m_D=class="num">0;
  }
class="type">void ISpiro::SetData(class="type">class="kw">double aR,class="type">class="kw">double ar,class="type">class="kw">double aD,class="type">class="kw">double adAlpha,class="type">class="kw">double anRotate)
  {
   m_R=aR; m_r=ar; m_D=aD; m_dAlfa=adAlpha; m_nRotate=anRotate;
  }
class="type">void ISpiro::DrawSpiro()
  {
   if(m_r<=class="num">0) { Print("Error! r==class="num">0"); class="kw">return; }
   if(m_D<=class="num">0) { Print("Error! D==class="num">0"); class="kw">return; }
   if(m_dAlfa==class="num">0) { Print("Error! Alpha==class="num">0"); class="kw">return; }
   ObjectsDeleteAll2(class="num">0,class="num">0,-class="num">1,m_sID+"pnt.");
   class="type">int n=class="num">0; class="type">class="kw">double a=class="num">0;
   class="kw">while(a<m_nRotate*class="num">2*class="num">3.1415926)
    {
     class="type">class="kw">double x=(m_R-m_r)*MathCos(a)+m_D*MathCos((m_R-m_r)/m_r*a);
     class="type">class="kw">double y=(m_R-m_r)*MathSin(a)-m_D*MathSin((m_R-m_r)/m_r*a);
     SetPoint(class="type">int(m_x0+x),class="type">int(m_y0+y));
     a+=m_dAlfa;
    }

「用导入库在EA里画自定义图形」

把绘图逻辑编译成独立 ex5 再 import 进 EA,是 MT5 里隔离界面代码的有效做法。上面这段来自一个叫 Spiro 的 EA 目标文件,它从 Graph.ex5 导入了 SetLabel、SetEdit、SetButton、HideChart 四个函数,又从 ISpiro.ex5 导入了工厂函数 iSpiro() 来拿到 CSpiro 实例。 OnInit 里先 HideChart(0, clrWhite) 把主图背景刷白,再给对象名加 spiro. 前缀避免冲突。接着 spiro.Init(250, 200, clrBlack, sID) 设定画布中心与尺寸,SetData(100, 30, 40, 0.04, 10) 给的是半径、转速差、步长等五个参数,最后 DrawSpiro() 出图。 这种拆法让你改图形算法只重编 ISpiro,不动 EA 主体。外汇与贵金属图表加载此类对象有内存开销,参数设太大(如 Init 超过 500 像素)在低频帧账户上可能拖慢刷新。

MQL5 / C++
class=class="str">"cmt">//--- Spiro.mq5 - the target file of the Expert Advisor
class=class="str">"cmt">//--- importing some functions
class="macro">#class="kw">import "Graph.ex5"
  class="type">void SetLabel(class="type">long achart, class="type">class="kw">string name, class="type">int wnd, class="type">class="kw">string text, class="type">color clr,
  class="type">int x, class="type">int y, class="type">int corn=class="num">0, class="type">int fontsize=class="num">8, class="type">class="kw">string font="Tahoma");
  class="type">void SetEdit(class="type">long achart, class="type">class="kw">string name, class="type">int wnd, class="type">class="kw">string text, class="type">color txtclr, class="type">color bgclr, class="type">color brdclr,
  class="type">int x, class="type">int y, class="type">int dx, class="type">int dy, class="type">int corn=class="num">0, class="type">int fontsize=class="num">8, class="type">class="kw">string font="Tahoma", class="type">bool ro=false);
  class="type">void SetButton(class="type">long achart, class="type">class="kw">string name, class="type">int wnd, class="type">class="kw">string text, class="type">color txtclr, class="type">color bgclr,
  class="type">int x, class="type">int y, class="type">int dx, class="type">int dy, class="type">int corn=class="num">0, class="type">int fontsize=class="num">8, class="type">class="kw">string font="Tahoma", class="type">bool state=false);
  class="type">void HideChart(class="type">long achart, class="type">color BackClr);
class="macro">#class="kw">import
class=class="str">"cmt">//--- including the chart class
class="macro">#include <Spiro.mqh>

class=class="str">"cmt">//--- importing the object
class="macro">#class="kw">import "ISpiro.ex5"
  CSpiro *iSpiro();
class="macro">#class="kw">import
class=class="str">"cmt">//--- object instance
CSpiro *spiro;
class=class="str">"cmt">//--- prefix for chart objects
class="type">class="kw">string sID;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnInit()
{
  HideChart(class="num">0, clrWhite);
  sID="spiro.";
  DrawParam();
class=class="str">"cmt">//--- object instance created
  spiro=iSpiro();
class=class="str">"cmt">//--- initializing the drawing
  spiro.Init(class="num">250, class="num">200, clrBlack, sID);
class=class="str">"cmt">//--- setting the calculation parameters
  spiro.SetData(class="num">100, class="num">30, class="num">40, class="num">0.04, class="num">10);
class=class="str">"cmt">//--- drawing
  spiro.DrawSpiro();
}

◍ 卸载指标时释放指针

MQL5 指标或 EA 在终端移除、切换周期、重新编译时都会触发 OnDeinit。若构造阶段用 new 创建了对象却不在退出时清理,MT5 会留下无法回收的堆内存,多次加载后可能拖慢客户端。 上面这段处理函数只做一件事:用 delete spiro 把名为 spiro 的自定义对象销毁。注释写的是 deleting the object,即释放该指针占用的资源,避免句柄泄漏。 实盘环境里,贵金属与外汇品种波动剧烈、图表频繁重绘,指标被反复加载卸载的概率高于股票周期;养成在 OnDeinit 显式 delete 的习惯,能降低 MT5 卡顿的可能性。

MQL5 / C++
class="type">void OnDeinit(const class="type">int reason)
{
   class="kw">delete spiro; class=class="str">"cmt">// deleting the object
}

跨 ex5 模块的变量初始化套路

把 ISuperClass 藏进 .ex5 做库文件后,最麻烦的是全局变量怎么从主程序喂进去。globals.mqh 里用 extern 声明的 CTrade *_trade 这类指针,不能在库里自己 new,只能由外部传指针进来,否则隐藏实现文件编译时会找不到实例。 最简单的做法是把指向对象的指针从 ISuperClass 类直接递给 .ex5 文件:外部初始化好 _trade,库函数接收后赋值,所有依赖全局变量的逻辑就能跑通。示例 7 里 iSuperClass(CTrade *atrade) 导出函数就干这件事——_trade=atrade 一行完成注入,return(new ISuperClass) 把隐藏类实例抛回。 全局变量一多,反复改函数头很烦。更好的办法是聚成一个专用类,把 extern 参数全塞进去。示例 8 的 __extern 类就是这么干的:trade 和 eaname 两个成员,Get() 从 extern 拉值,Set() 往 extern 写回,_GetExt() 返回类指针,主程序只调一次 iSuperClass(_GetExt()) 就能把整包变量送进库(示例 10)。 这套结构在 MT5 里实测可行:把 ISuperClass.mq5 标 #property library 编译成 ex5,主脚本用 #import 拽进来,OnStart 里一行拿到 CSuperClass 指针即可。外汇与贵金属 EA 用这种模块化拆法时,注意库更新要重编译依赖方,高风险品种下参数错传可能导致下单异常。

MQL5 / C++
Example class="num">6. Public include file
class=class="str">"cmt">//--- globals.mqh
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//--- instance of the trade function object
class="kw">extern CTrade *_trade; 
Example class="num">7. Initialization of variables upon creation of the object
class=class="str">"cmt">//--- ISuperClass.mq5 –hidden implementation file
class="macro">#class="kw">property library
CSuperClass *iSuperClass(CTrade *atrade) class="kw">export
{
class=class="str">"cmt">//--- saving the pointer
  _trade=atrade; 
class=class="str">"cmt">//--- returning the object of the hidden implementation of ISuperClass of the open CSuperClass class
  class="kw">return(new ISuperClass); 
}
class=class="str">"cmt">//... the remaining code
Example class="num">8. Public include file
class=class="str">"cmt">//--- globals.mqh
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//--- trade "object"
class="kw">extern CTrade *_trade; 
class=class="str">"cmt">//--- name of the Expert Advisor of the system
class="kw">extern class="type">class="kw">string _eaname; 

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| class __extern                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class __extern class=class="str">"cmt">// all class="kw">extern parameters for passing between the ex5 modules are accumulated here
{
class="kw">public:
class=class="str">"cmt">//--- the list of all class="kw">public global variables to be passed
class=class="str">"cmt">//--- trade "object"
  CTrade *trade; 
class=class="str">"cmt">//--- name of the Expert Advisor of the system
  class="type">class="kw">string eaname; 
   
class="kw">public:
  __extern() { };
  ~__extern() { };
class=class="str">"cmt">//--- it is called when passing the parameters into the .ex5 file
  class="type">void Get() { trade=_trade; eaname=_eaname; };  class=class="str">"cmt">// getting the variables
class=class="str">"cmt">//--- it is called in the .ex5 file
  class="type">void Set() { _trade=trade; _eaname=eaname; };  class=class="str">"cmt">// setting the variables
                                                     
};
class=class="str">"cmt">//--- getting the variables and pointer for passing the object into the .ex5 file
__extern *_GetExt() { _ext.Get(); class="kw">return(GetPointer(_ext)); } 
class=class="str">"cmt">//--- the only instance for operation
class="kw">extern __extern _ext; 
Example class="num">9.
class=class="str">"cmt">//--- ISuperClass.mq5 –hidden implementation file
class="macro">#class="kw">property library
CSuperClass *iSuperClass(__extern *aext) class="kw">export
{
class=class="str">"cmt">//--- taking in all the parameters
  aext.Set();
class=class="str">"cmt">//--- returning the object
  class="kw">return(new ISuperClass); 
}
class=class="str">"cmt">//--- ... the remaining code
Example class="num">10. Using class="kw">export objects in the presence of class="kw">public global variables
class=class="str">"cmt">//--- including global variables(usually located in SuperClass.mqh)
class="macro">#include "globals.mqh"   
class=class="str">"cmt">//--- including the class="kw">public header class
class="macro">#include "SuperClass.mqh"
class=class="str">"cmt">//--- getting the hidden implementation object
class="macro">#class="kw">import "ISuperClass.ex5"
  CSuperClass *iSuperClass();
class="macro">#class="kw">import

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
class=class="str">"cmt">//--- creating the hidden implementation object providing for the passing of all parameters
  CSuperClass *sc=iSuperClass(_GetExt()); 
  class=class="str">"cmt">//--- ... the remaining code
}
交给小布盯盘看工程结构
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到库依赖与导出符号的异常提示,你专注决策逻辑而非编译路径。

常见问题

当前语言规范只支持函数级导出,类需拆成公共声明与隐藏实施两部分,通过返回对象指针间接暴露接口。
不完全一致,库加载时机由导入指令触发,静态变量应在被调用前显式初始化,避免跨模块状态错乱。
只要基类声明与库内实施保持 ABI 一致,虚调度可正常工作,但版本错配会导致未定义行为,倾向锁定编译器版本。
可以,小布会在品种页标记导入路径解析失败与库版本偏移,省去你手动翻 Experts 日志的时间。
必须附带仅含声明的 mqh,否则第三方无法编译导入;实施文件保留 ex5 即可,外汇贵金属策略涉及高风险,分发前确认授权边界。