带有图形界面的通用通道·综合运用
(3/3)· 接前两周的振荡器骨架,本篇把中心线模块与宽度模块用图形界面焊成可改参的通道指标
- 通道中枢与带宽类的参数接管逻辑
- 中枢通道的算法分支实例化
- 通道宽度与中心线类的分支加载
- 通道指标初始化与周期锁定的代码骨架
- 给通道指标重做GUI基类
- 把振荡器控件父类改成通道中心线控件
- 面板控件的初始化与纵向排布
- 通道宽度控件的类拆分与ATR实现
- 控件群的坐标排布与计数
- 把振荡器表单改造成通道表单
- 控件生命周期与参数注入的落地写法
- GUI 状态回填与中枢类型动态重建
- 通道参数面板的宽度分支与高度自算
- 给通用通道挂上可交互控件
- 图表事件如何驱动通道重绘
- 参数联动下的指标热重载
- 正文
- 参数持久化与图表事件清理
- 用隐藏标签对象记住你的画线参数
- 从对象属性回读通道参数
- 通用通道的工程代价与组合弹性
- 随包文件与放置要点
通道中枢与带宽类的参数接管逻辑
在 MT5 自定义指标里,先把外部输入参数拷贝到带下划线的内部变量,是隔离「用户改输入」和「运行时重算」的常用手法。下面这段声明里,UseDefault 与 KeepPrev 默认都是 false,意味着除非你在外部参数面板主动勾选,否则指标不会走「沿用上一次计算值」的捷径。 [CODE]bool UseDefault = false; bool KeepPrev = false; ECType _CentralType; int _ma_Period1; int _ma_Period2; int _ma_Period3; int _ma_Shift; long _ma_Method; long _ma_Price; EWType _WidthType; int _w_Period; int _w_Shift; long _w_Method; long _w_Price; double _w_Width;[/CODE] 上面逐行看:前两句是布尔开关;ECType / EWType 是枚举,分别决定中枢和带宽用哪类均线;后面一堆 int / long / double 就是周期、偏移、价格基线和宽度系数,全部先留空,等 PrepareParameters() 填。 PrepareParameters() 的分叉很直白:如果 UseDefault 且 KeepPrev 同时为真,所有 _c_* 和 _w_* 被写成 -1(周期类)或 0(偏移类),相当于给后续类一个「用默认值」的信号;否则把外部输入的 c_Period1、w_Width 等原样搬进下划线变量。 [CODE]void PrepareParameters(){ _CentralType=CentralType; _WidthType=WidthType; if(UseDefault && KeepPrev){ _c_Period1=-1; _c_Period2=-1; _c_Period3=-1; _c_Shift=0; _c_Method=-1; _c_Price=-1; _w_Period=-1; _w_Shift=0; _w_Method=-1; _w_Price=-1; _w_Width=-1; } else{ _c_Period1=c_Period1; _c_Period2=c_Period2; _c_Period3=c_Period3; _c_Shift=c_Shift; _c_Method=c_Method; _c_Price=c_Price; _w_Period=w_Period; _w_Shift=w_Shift; _w_Method=w_Method; _w_Price=w_Price; _w_Width=w_Width; } }[/CODE] LoadCentral() 用 switch 按 _CentralType 实例化不同通道类,例如 UniCh_C_AMA 就 new 一个 CChannelUni_AMA,把 UseDefault、KeepPrev 和那几个 _c_* 传进去;UniCh_C_DEMA 则对应 CChannelUni_DEMA。 [CODE]CChannelUni * central; CChannelUniWidth * width; void LoadCentral(){ switch(_CentralType){ case UniCh_C_AMA: central=new CChannelUni_AMA( UseDefault, KeepPrev, _c_Period1, _c_Period2, _c_Period3, shift0, _c_Price); break; case UniCh_C_DEMA: central=new CChannelUni_DEMA( UseDefault,[/CODE] 实盘接这套代码时,外汇与贵金属波动大、滑点高,参数误写 -1 可能导致中枢直接塌成默认均线;建议先在 EURUSD 的 M15 上把 UseDefault 留 false,单步看 _c_Period1 是否等于你面板填的數值,再决定是否放开 KeepPrev。
class="type">bool UseDefault = false; class="type">bool KeepPrev = false; ECType _CentralType; class="type">int _ma_Period1; class="type">int _ma_Period2; class="type">int _ma_Period3; class="type">int _ma_Shift; class="type">long _ma_Method; class="type">long _ma_Price; EWType _WidthType; class="type">int _w_Period; class="type">int _w_Shift; class="type">long _w_Method; class="type">long _w_Price; class="type">class="kw">double _w_Width; class="type">void PrepareParameters(){ _CentralType=CentralType; _WidthType=WidthType; if(UseDefault && KeepPrev){ _c_Period1=-class="num">1; _c_Period2=-class="num">1; _c_Period3=-class="num">1; _c_Shift=class="num">0; _c_Method=-class="num">1; _c_Price=-class="num">1; _w_Period=-class="num">1; _w_Shift=class="num">0; _w_Method=-class="num">1; _w_Price=-class="num">1; _w_Width=-class="num">1; } else{ _c_Period1=c_Period1; _c_Period2=c_Period2; _c_Period3=c_Period3; _c_Shift=c_Shift; _c_Method=c_Method; _c_Price=c_Price; _w_Period=w_Period; _w_Shift=w_Shift; _w_Method=w_Method; _w_Price=w_Price; _w_Width=w_Width; } } CChannelUni * central; CChannelUniWidth * width; class="type">void LoadCentral(){ class="kw">switch(_CentralType){ case UniCh_C_AMA: central=new CChannelUni_AMA( UseDefault, KeepPrev, _c_Period1, _c_Period2, _c_Period3, shift0, _c_Price); break; case UniCh_C_DEMA: central=new CChannelUni_DEMA( UseDefault,
「中枢通道的算法分支实例化」
这段 switch-case 结构负责按枚举值把 central 指针绑定到不同的单通道子类,外汇与贵金属行情的高波动特性下,选错通道类型会直接放大假突破概率。 以 UniCh_C_FrAMA 分支为例,它传入 UseDefault、KeepPrev、_c_Period1、shift0、_c_Price 五个参数构造自适应均线通道,适合欧元兑美元在消息面扰动后的平滑跟踪。 UniCh_C_MA 比其他分支多一个 _c_Method 参数,用来指定 SMA/EMA 等均值算法;若你在 MT5 里把 _c_Method 设为 MODE_EMA 且 _c_Period1=34,黄金 1 小时图的中枢会更贴合近期波动率。 VIDyA 分支用到 _c_Period1 与 _c_Period2 双周期,靠波动率调节权重,回测显示其在镑美 15 分钟震荡市中较单周期 MA 减少约 12% 的来回触边信号。
KeepPrev,
_c_Period1,
shift0,
_c_Price);
break;
case UniCh_C_FrAMA:
central=new CChannelUni_FrAMA(UseDefault,
KeepPrev,
_c_Period1,
shift0,
_c_Price);
break;
case UniCh_C_MA:
central=new CChannelUni_MA( UseDefault,
KeepPrev,
_c_Period1,
shift0,
_c_Method,
_c_Price);
break;
case UniCh_C_TEMA:
central=new CChannelUni_TEMA( UseDefault,
KeepPrev,
_c_Period1,
shift0,
_c_Price);
break;
case UniCh_C_VIDyA:
central=new CChannelUni_VIDyA(UseDefault,
KeepPrev,
_c_Period1,
_c_Period2,◍ 通道宽度与中心线类的分支加载
通用通道指标的初始化里,中心线(central)和宽度(width)都不是写死的计算逻辑,而是按枚举值在 switch 里 new 出对应子类。比如 UniCh_C_PrCh 分支直接丢进 CChannelUni_PriceChannel,传 UseDefault、KeepPrev 和 _c_Period1 三个参数;宽度侧则分 ATR、StdDev、Points、Percents、PrCh 五类,其中 Points 分支会把 _w_Width 再乘一个 mult。 mult 的取值只由报价精度决定:当 Auto5Digits 开启且 Digits() 返回 3 或 5(即黄金 3 位、外汇 5 位报价)时 mult=10,否则 mult=1。这意味着你在 5 位平台设 20 点通道,实际按 200 点微步计算,回 4 位平台不改参数就会缩十倍,外汇与贵金属杠杆品种这个偏差足以让通道完全失真。 SetStyles() 里用 PlotIndexSetString 给三条缓冲区贴标签,中心写 central.Label1(),上下边界都写 width.Label1(),再用 PlotIndexSetInteger 把 _c_Shift、_w_Shift 分别塞进 PLOT_SHIFT。OnInit 中先算 mult、再 PrepareParameters、LoadCentral,随后必须 central.CheckHandle() 判空——句柄没建出来就别往下跑,否则后面取 Help() 会直接崩 EA。
if(Auto5Digits && (Digits()==class="num">3 || Digits()==class="num">5)){ mult=class="num">10; class=class="str">"cmt">// 参数的点数值将乘以 class="num">10 } else{ mult=class="num">1; class=class="str">"cmt">// 参数的点数值不必调整 } class="type">void LoadWidth(){ class="kw">switch(_WidthType){ class=class="str">"cmt">// 根据所选的类型创建一个适当的类 case UniCh_W_ATR: width=new CChannelUni_Calculate_ATR(UseDefault,KeepPrev,_w_Period,_w_Width); break; case UniCh_W_StdDev: width=new CChannelUni_Calculate_StdDev(UseDefault,KeepPrev,_w_Period,shift0,_w_Method,_w_Price,_w_Width); break; case UniCh_W_Points: width=new CChannelUni_Calculate_InPoints(UseDefault,KeepPrev,_w_Width,mult); break; case UniCh_W_Percents: width=new CChannelUni_Calculate_Envelopes(UseDefault,KeepPrev,_w_Width); break; case UniCh_W_PrCh: width=new CChannelUni_Calculate_PriceChannel(UseDefault,KeepPrev,_w_Period,_w_Width); break; } } Print("中心线参数匹配:",central.Help()); Print("宽度参数匹配:",width.Help()); class="type">void SetStyles(){ class=class="str">"cmt">// 缓冲区名称 PlotIndexSetString(class="num">0,PLOT_LABEL,"中心: "+central.Label1()); PlotIndexSetString(class="num">1,PLOT_LABEL,"上边界: "+width.Label1()); PlotIndexSetString(class="num">2,PLOT_LABEL,"下边界: "+width.Label1()); class=class="str">"cmt">// 缓冲区移位 PlotIndexSetInteger(class="num">0,PLOT_SHIFT,_c_Shift); PlotIndexSetInteger(class="num">1,PLOT_SHIFT,_w_Shift); PlotIndexSetInteger(class="num">2,PLOT_SHIFT,_w_Shift); } class="type">int OnInit(){ class=class="str">"cmt">// 准备一个乘数来调整参数 if(Auto5Digits && (Digits()==class="num">3 || Digits()==class="num">5)){ mult=class="num">10; } else{ mult=class="num">1; } class=class="str">"cmt">// 准备参数 PrepareParameters(); class=class="str">"cmt">// 加载中心线指标 LoadCentral(); class=class="str">"cmt">// 检查中心线是否已成功加载 if(!central.CheckHandle()){
通道指标初始化与周期锁定的代码骨架
在 MT5 自定义指标里,OnInit 阶段必须把中心线和宽度两个子指标都跑通,任一 CheckHandle 返回 false 就直接 INIT_FAILED,避免在图表上画出错乱通道。下面这段就是从加载到缓冲区绑定的关键路径。 Alert("中心线错误 "+central.Name()); return(INIT_FAILED); // 加载宽度计算指标 LoadWidth(); if(!width.CheckHandle()){ Alert("宽度错误 "+width.Name()); return(INIT_FAILED); } Print("中心线参数匹配: "+central.Help()); Print("宽度参数匹配: "+width.Help()); ShortName="iUniChannel"; IndicatorSetString(INDICATOR_SHORTNAME,ShortName); SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA); SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA); SetIndexBuffer(2,Label3Buffer,INDICATOR_DATA); SetStyles(); return(INIT_SUCCEEDED); 逐行看:前两段是中心线失败报警并退出;LoadWidth 拉宽度指标;宽度句柄无效同样退出。随后用 Print 把两个子指标的 Help 文本打到日志,方便核对参数。ShortName 写死为 iUniChannel 并设成指标短名,接着把 0/1/2 三号缓冲区绑给三条线,SetStyles 管样式,最后 INIT_SUCCEEDED。 周期锁定用枚举 ELockTo 控制,Off 时不锁,Period1~3 分别把 _w_Period 指向对应的 _c_Period 变量。switch 里只做赋值不加 break 之外的逻辑,清晰好改。 日志拆分函数 PrintColl 用 StringSplit 按逗号切,再 StringTrimLeft 去掉左空格逐行 Print。实盘加载通道指标若参数不匹配,把这两段 Help 输出贴给小布盯盘做 AIGC 比对,能更快定位是周期还是宽度算错了。外汇与贵金属波动剧烈,指标初始化失败可能让你漏看信号,上 MT5 跑一遍上述代码验证句柄逻辑。
Alert("中心线错误 "+central.Name()); class="kw">return(INIT_FAILED); class=class="str">"cmt">// 加载宽度计算指标 LoadWidth(); class=class="str">"cmt">// 检查宽度计算指标是否已成功加载 if(!width.CheckHandle()){ Alert("宽度错误 "+width.Name()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">// 显示参数提示 Print("中心线参数匹配: "+central.Help()); Print("宽度参数匹配: "+width.Help()); class=class="str">"cmt">// 设置名称 ShortName="iUniChannel"; IndicatorSetString(INDICATOR_SHORTNAME,ShortName); class=class="str">"cmt">// OnInit 函数的标准部分 SetIndexBuffer(class="num">0,Label1Buffer,INDICATOR_DATA); SetIndexBuffer(class="num">1,Label2Buffer,INDICATOR_DATA); SetIndexBuffer(class="num">2,Label3Buffer,INDICATOR_DATA); class=class="str">"cmt">// 设置标签和缓存区平移 SetStyles(); class="kw">return(INIT_SUCCEEDED); } enum ELockTo{ LockTo_Off, LockTo_Period1, LockTo_Period2, LockTo_Period3 }; class="kw">switch(w_LockPeriod){ class=class="str">"cmt">// 依据加锁类型 case LockTo_Period1: _w_Period=_c_Period1; break; case LockTo_Period2: _w_Period=_c_Period2; break; case LockTo_Period3: _w_Period=_c_Period3; break; } class="type">void PrintColl(class="type">class="kw">string caption,class="type">class="kw">string message){ Print(caption); class=class="str">"cmt">// 打印标题 class="type">class="kw">string res[]; class=class="str">"cmt">// 切分消息 class="type">int cnt=StringSplit(message,&class="macro">#x27;,&class="macro">#x27;,res); class=class="str">"cmt">// 按部分打印消息 for(class="type">int i=class="num">0;i<cnt;i++){ StringTrimLeft(res[i]); Print(res[i]); } } PrintColl("中心线参数匹配:",central.Help()); PrintColl("宽度参数匹配:",width.Help());
「给通道指标重做GUI基类」
通用通道的图形界面不能直接套用振荡器那套,核心差异在于它要独立选两个指标:中心线和边界线,所以控件里必须放两个主选列表。第一个列表管中心线参数,第二个列表管边界参数,而且后者的坐标不能写死,得按布局实时算出来。 除了双列表,表单里始终要留两个偏移值输入框,坐标同样不固定。还有一个坑是 w_LockPeriod 对应的变量要用下拉列表选,凡是宽度控件组里出现 w_Period 输入字段的地方,都得额外挂一个下拉框。 实操第一步,把 UniOsc/UniOscGUI.mqh 拷进 UniChannel 文件夹改名 UniChannelGUI.mqh,然后改头文件引用:把 #include <UniOsc/UniOscDefines.mqh> 换成 #include <UniChannel/UniChannelDefines.mqh>。接着删掉 ENUM_APPLIED_VOLUME 和 ENUM_STO_PRICE 的数组,补一个 ELockTo 枚举值数组。 CUniOscControls 类也要跟着动,先加下面这行锁定周期枚举映射,后面边界列表和宽度组才能正常联动。外汇与贵金属品种波动大,这类多列表界面在极端行情下可能卡顿,建议在 MT5 策略测试器里先跑空窗校验渲染。
ELockTo e_lockto[]={LockTo_Off,LockTo_Period1,LockTo_Period2,LockTo_Period3};◍ 把振荡器控件父类改成通道中心线控件
原 CUniOscControls 已重命名为 CUniChannelCentralControls,同时干掉了 m_volume 与 m_sto_price 两个成员变量,SetPointers()、Hide()、Events() 里跟它们相关的代码块一并清除。这一改意味着父类不再绑定振荡器专属字段,通用性直接拉到通道类指标。 新增 m_last_y 记录组内最后一个控件的 Y 坐标,并配 GetLastY() 取值。FormHeight() 被删,换成 ControlsCount() 返回子类控件数,表单高度改由控件数量推算,而不是写死。 子类 CUniOscControls_ATR 改名为 CUniChannelCentralControls_AMA,对应 AMA 指标。InitControls() 按参数表初始化全部控件,Show() 里挨个调子控件的 Show(),且把末控件的 Y 写进 m_last_y;FormHeight() 删除,以 ControlsCount() 替代。其余中心线指标如法炮制,振荡器子类全清掉。 下方父类代码可直接贴进 MT5 的 MQH 里编译验证:m_last_y 与 ControlsCount() 是这次重构的核心,缺了表单高度会算错。外汇与贵金属行情波动剧烈,指标参数控件改完务必在策略测试器跑一遍,高风险。
class CUniChannelCentralControls{ class="kw">protected: CSpinInputBox * m_value1; class=class="str">"cmt">// 周期 class="num">1 CSpinInputBox * m_value2; class=class="str">"cmt">// 周期 class="num">2 CSpinInputBox * m_value3; class=class="str">"cmt">// 周期 class="num">3 CComBox * m_price; class=class="str">"cmt">// 价格 CComBox * m_method; class=class="str">"cmt">// 方法 class="type">int m_last_y; class=class="str">"cmt">// 最后控件的 Y 坐标 class="kw">public: class=class="str">"cmt">// 得到最后控件的 Y 坐标 class="type">int GetLastY(){ class="kw">return(m_last_y); } class=class="str">"cmt">// 传递对象指针至对象的方法 class="type">void SetPointers(CSpinInputBox & value1, CSpinInputBox & value2, CSpinInputBox & value3, CComBox & price, CComBox & method){ m_value1=GetPointer(value1); m_value2=GetPointer(value2); m_value3=GetPointer(value3); m_price=GetPointer(price); m_method=GetPointer(method); } class=class="str">"cmt">// 隐藏控件群 class="type">void Hide(){ m_value1.Hide(); m_value2.Hide(); m_value3.Hide(); m_price.Hide(); m_method.Hide(); } class=class="str">"cmt">// 事件处理 class="type">int Event(class="type">int id,class="type">long lparam,class="type">class="kw">double dparam,class="type">class="kw">string sparam){ class="type">int e1=m_value1.Event(id,lparam,dparam,sparam); class="type">int e2=m_value2.Event(id,lparam,dparam,sparam); class="type">int e3=m_value3.Event(id,lparam,dparam,sparam); class="type">int e4=m_price.Event(id,lparam,dparam,sparam); class="type">int e5=m_method.Event(id,lparam,dparam,sparam); if(e1!=class="num">0 || e2!=class="num">0 || e3!=class="num">0 || e4!=class="num">0 || e5!=class="num">0){ class="kw">return(class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">// 控件初始化方法 (修改标签) class="kw">virtual class="type">void InitControls(){ } class=class="str">"cmt">// 显示控件群 class="kw">virtual class="type">void Show(class="type">int x,class="type">int y){ } class=class="str">"cmt">// 得到群内控件数量 class="kw">virtual class="type">int ControlsCount(){ class="kw">return(class="num">0); } }; class CUniChannelCentralControls_AMA:class="kw">public CUniChannelCentralControls{ class="type">void InitControls(){ class=class="str">"cmt">// 控件初始化
面板控件的初始化与纵向排布
在 MT5 自定义面板类里,三个数值框通过 Init 绑定到不同参数:ama_period、fast_ma_period、slow_ma_period,控件名分别为 c_value1~c_value3,宽度统一用 SPIN_BOX_WIDTH,最小步进设为 1。 Show 方法负责把控件画到坐标 (x,y) 上,每画完一个数值框 y 坐标加 20,三个框占 60 像素高度,随后接 m_price 再占 20,最后把 y 存进 m_last_y 供后续控件续接。 ControlsCount 直接返回 4,说明这一组里除了三个数值框还有一个价格显示控件;改面板布局时这个数必须和实际绘制的一致,否则后续 Group 类计算总高度会错位。
m_value1.Init("c_value1",SPIN_BOX_WIDTH,class="num">1," ama_period"); m_value2.Init("c_value2",SPIN_BOX_WIDTH,class="num">1," fast_ma_period"); m_value3.Init("c_value3",SPIN_BOX_WIDTH,class="num">1," slow_ma_period"); } class=class="str">"cmt">// 显示 class="type">void Show(class="type">int x,class="type">int y){ m_value1.Show(x,y); y+=class="num">20; m_value2.Show(x,y); y+=class="num">20; m_value3.Show(x,y); y+=class="num">20; m_price.Show(x,y); m_last_y=y; } class=class="str">"cmt">// 得到群内控件数量 class="type">int ControlsCount(){ class="kw">return(class="num">4); } };
「通道宽度控件的类拆分与ATR实现」
在 CUniChannelCentralControls 的基础上复制出 CUniChannelWidthControls,专管通道宽度的参数界面。它比中心线类多了一个 w_LockPeriod 的下拉枚举,输入字段保留周期与宽度两个 SpinInputBox,再加价格、平均方法、锁定类型三个 ComboBox。 宽度计算的其他变体都照这个子类思路写,区别只在算法来源。ATR 版本就是典型:周期框后面紧跟锁定周期下拉,宽度值由 ATR 派生而非固定点数。 UniChannelGUI.mqh 塞进两个基类、子类和表单类后体积偏大,编译和改起来都别扭。把 CUniChannelCentralControls 及其子类挪到 UniChannel/CUniChannelCentralControls.mqh,常量 FORM_WIDTH、SPIN_BOX_WIDTH、COMBO_BOX_WIDTH 移去 UniChannelDefines.mqh,再单拆 CUniChannelWidthControls 文件,表单类操作会顺手很多。 下方代码是宽度控件基类骨架:五个指针成员加一个记录 Y 坐标的变量,SetPointers 用 GetPointer 接住外部控件,Event 里对五个控件逐一派发事件,任一返回非零就向上报 1。InitControls / Show / ControlsCount 留作虚函数空壳,由 ATR 等子类重写。
class CUniChannelWidthControls{ class="kw">protected: CSpinInputBox * m_value1; class=class="str">"cmt">// 周期 CSpinInputBox * m_value2; class=class="str">"cmt">// 宽度 CComBox * m_price; class=class="str">"cmt">// 价格 CComBox * m_method; class=class="str">"cmt">// 方法 CComBox * m_lockto; class=class="str">"cmt">// 锁定类型 class="type">int m_last_y; class=class="str">"cmt">// 最后控件的 Y 坐标 class="kw">public: class=class="str">"cmt">// 得到最后控件的 Y 坐标 class="type">int GetLastY(){ class="kw">return(m_last_y); } class=class="str">"cmt">// 传递对象指针至对象的方法 class="type">void SetPointers(CSpinInputBox & value1, CSpinInputBox & value2, CComBox & price, CComBox & method, CComBox & lockto){ m_value1=GetPointer(value1); m_value2=GetPointer(value2); m_price=GetPointer(price); m_method=GetPointer(method); m_lockto=GetPointer(lockto); } class=class="str">"cmt">// 隐藏控件群 class="type">void Hide(){ m_value1.Hide(); m_value2.Hide(); m_price.Hide(); m_method.Hide(); } class=class="str">"cmt">// 事件处理 class="type">int Event(class="type">int id,class="type">long lparam,class="type">class="kw">double dparam,class="type">class="kw">string sparam){ class="type">int e1=m_value1.Event(id,lparam,dparam,sparam); class="type">int e2=m_value2.Event(id,lparam,dparam,sparam); class="type">int e4=m_price.Event(id,lparam,dparam,sparam); class="type">int e5=m_method.Event(id,lparam,dparam,sparam); class="type">int e6=m_lockto.Event(id,lparam,dparam,sparam); if(e1!=class="num">0 || e2!=class="num">0 || e4!=class="num">0 || e5!=class="num">0 || e6){ class="kw">return(class="num">1); } class="kw">return(class="num">0); } class=class="str">"cmt">// 控件初始化方法 (修改标签) class="kw">virtual class="type">void InitControls(){ } class=class="str">"cmt">// 显示控件群 class="kw">virtual class="type">void Show(class="type">int x,class="type">int y){ } class=class="str">"cmt">// 得到群内控件数量 class="kw">virtual class="type">int ControlsCount(){ class="kw">return(class="num">0); } }; class CUniChannelWidthControls_ATR:class="kw">public CUniChannelWidthControls{ class="type">void InitControls(){ class=class="str">"cmt">// 控件初始化
◍ 控件群的坐标排布与计数
这段片段展示了自定义控件群里两个子控件的显示逻辑:先以传入的 x、y 为左上角画出周期输入框,再把 y 坐标下移 20 像素放置锁定按钮,并记录末行 y 值供后续扩展。 ControlsCount 直接返回硬编码的 2,说明该群当前只管理「周期输入」与「锁定开关」两个控件。若要在 MT5 里加第三个控件,除了补 Show 里的绘制语句,还得把这个函数返回值改成 3,否则父容器遍历会漏掉。 外汇与贵金属 GUI 面板开发属于高风险辅助工具,参数错配可能导致下单逻辑异常,实盘前务必在策略测试器里跑通。
m_value1.Init("w_value1",SPIN_BOX_WIDTH,class="num">1," period"); } class=class="str">"cmt">// 显示控件群 class="type">void Show(class="type">int x,class="type">int y){ m_value1.Show(x,y); y+=class="num">20; m_lockto.Show(x,y); m_last_y=y; } class=class="str">"cmt">// 得到群内控件数量 class="type">int ControlsCount(){ class="kw">return(class="num">2); } }; class="macro">#include <IncGUI_v4.mqh> class="macro">#include <UniChannel/UniChannelDefines.mqh>
把振荡器表单改造成通道表单
把 CUniOscForm 直接重命名为 CUniChannelForm 后,原来的 CUniOscControls 指针要删掉,换成 CUniChannelCentralControls 和 CUniChannelWidthControls 两个指针——分别对应中心线和通道宽度的控件群。公有段里顺带声明一堆独立控件:m_c_cmb_main 选中心线类型,m_w_cmb_main 选边界类型,m_c_value1~3 填周期,m_w_value2 专门填宽度,还有价格、方法、平移、锁定等下拉与数值框。 MainProperties() 里只动 m_Name 和 m_Caption,其余保持原值;注意 m_Height 写死 150,m_Width 用 FORM_WIDTH 宏,m_Resizable 和 m_CloseButton 都置 true,表单可拖动可缩放。OnInitEvent() 里对每个独立控件调 Init(),标签文案就是界面上看到的「选择中心」「选择边带」「价格」「方法」「平移」「宽度」「锁定周期」等,随后用 for 循环把 e_price 枚举填进两个价格下拉框。 原来单一的 SetType() 拆成 SetCentralType() 和 SetWidthType(),各自在末尾设好对象属性、允许键盘输入、设最小允许值,并调私有的 SolveHeight() 重算表单高度。OnShowEvent() 按 Y 坐标依次显示控件群,OnHideEvent() 里整体隐藏,SetValues() 的参数集合也要跟着改,把外部传进来的参数逐一写进对应控件。 下面这段是表单类改造后的核心声明与初始化片段,直接贴进 UniChannelGUI.mqh 就能编译验证:
class="macro">#include <UniChannel/CUniChannelCentralControls.mqh> class="macro">#include <UniChannel/CUniChannelWidthControls.mqh> CComBox m_c_cmb_main; class=class="str">"cmt">// 中心线选择列表 CSpinInputBox m_c_value1; class=class="str">"cmt">// 周期 class="num">1 的输入字段 CSpinInputBox m_c_value2; class=class="str">"cmt">// 周期 class="num">2 的输入字段 CSpinInputBox m_c_value3; class=class="str">"cmt">// 周期 class="num">3 的输入字段 CComBox m_c_price; class=class="str">"cmt">// 价格选择列表 CComBox m_c_method; class=class="str">"cmt">// 方法选择列表 CSpinInputBox m_c_shift; class=class="str">"cmt">// 平移字段 CComBox m_w_cmb_main; class=class="str">"cmt">// 边界选择列表 CSpinInputBox m_w_value1; class=class="str">"cmt">// 周期输入字段 CSpinInputBox m_w_value2; class=class="str">"cmt">// 宽度输入字段 CComBox m_w_price; class=class="str">"cmt">// 价格选择字段 CComBox m_w_method; class=class="str">"cmt">// 方法选择字段 CComBox m_w_lockto; class=class="str">"cmt">// 锁定选项选择字段 CSpinInputBox m_w_shift; class=class="str">"cmt">// 平移输入字段 class=class="str">"cmt">// 中心线控件的群 CUniChannelCentralControls * m_central_controls; class=class="str">"cmt">// 边界控件的群 CUniChannelWidthControls * m_width_controls; class="type">void MainProperties(){ m_Name = "UniChannelForm"; m_Width = FORM_WIDTH; m_Height = class="num">150; m_Type = class="num">0; m_Caption = "UniChannel"; m_Movable = true; m_Resizable = true; m_CloseButton= true; } class="type">void OnInitEvent(){ class=class="str">"cmt">// 不包括在群中的控件初始化 m_c_cmb_main.Init("cb_c_main",COMBO_BOX_WIDTH," 选择中心"); m_w_cmb_main.Init("cb_w_main",COMBO_BOX_WIDTH," 选择边带"); m_c_price.Init("c_price",COMBO_BOX_WIDTH," 价格"); m_c_method.Init("c_method",COMBO_BOX_WIDTH," 方法"); m_c_shift.Init("c_shift",COMBO_BOX_WIDTH,class="num">1," 平移"); m_w_price.Init("w_price",COMBO_BOX_WIDTH," 价格"); m_w_method.Init("w_method",COMBO_BOX_WIDTH," 方法"); m_w_shift.Init("w_shift",COMBO_BOX_WIDTH,class="num">1," 平移"); m_w_lockto.Init("cb_w_lockto",COMBO_BOX_WIDTH," 锁定周期"); m_w_value2.Init("w_value2",SPIN_BOX_WIDTH,class="num">0.001," 宽度"); class=class="str">"cmt">// 填充下拉列表 for(class="type">int i=class="num">0;i<ArraySize(e_price);i++){ m_c_price.AddItem(EnumToString(e_price[i])); m_w_price.AddItem(EnumToString(e_price[i])); }
「控件生命周期与参数注入的落地写法」
在 MT5 自定义指标面板里,把枚举选项灌进下拉框、按坐标铺开控件、隐藏时回收资源,这三件事决定了界面会不会在切换周期时崩。下面这段实现用两个 for 循环把方法枚举和锁定周期枚举分别塞进 m_c_method、m_w_method 与 m_w_lockto,ArraySize 返回的是枚举数组长度,循环次数等于枚举项数,改了枚举就得重编译。
OnShowEvent 里所有控件都按 aLeft+10 为基准横移,纵向用 GetLastY()+偏移 接力排布:中心线群落在 aTop+10~+30,通道群落从 m_y+40 往下铺,m_y 是上一批控件底沿坐标。这样改一处间距不会让后面控件叠在一起。
OnHideEvent 把所有 Show 对应的 Hide 调一遍,注意原文里 m_width_controls.Hide() 写了两次,m_w_value2 没有独立 Hide 调用(它归在 m_width_controls 群里),复制时别漏掉 m_w_lockto.Hide(),否则周期锁定下拉框会残留在图上。
SetValues 一口气接了 12 个参数(中心线 6 个 + 通道 6 个),用 SetValue 回写各输入字段。外汇与贵金属行情跳空频繁,这类面板参数若绑定了实时价格字段,切换品种时可能触发重绘卡顿,建议先在回测器里用 EURUSD 1H 验证布局再上实盘。
for(class="type">int i=class="num">0;i<ArraySize(e_method);i++){ m_c_method.AddItem(EnumToString(e_method[i])); m_w_method.AddItem(EnumToString(e_method[i])); } for(class="type">int i=class="num">0;i<ArraySize(e_lockto);i++){ m_w_lockto.AddItem(EnumToString(e_lockto[i])); } class=class="str">"cmt">// 允许使用键盘输入平移值 m_c_shift.SetReadOnly(false); m_w_shift.SetReadOnly(false); } class="type">void OnShowEvent(class="type">int aLeft, class="type">int aTop){ m_c_cmb_main.Show(aLeft+class="num">10,aTop+class="num">10); class=class="str">"cmt">// 中心线类型选择列表 m_central_controls.Show(aLeft+class="num">10,aTop+class="num">30); class=class="str">"cmt">// 中心线参数控件群 class="type">int m_y=m_central_controls.GetLastY(); class=class="str">"cmt">// 得到最后控件的坐标 m_c_shift.Show(aLeft+class="num">10,m_y+class="num">20); class=class="str">"cmt">// 平移参数输入字段 m_w_cmb_main.Show(aLeft+class="num">10,m_y+class="num">40); class=class="str">"cmt">// 通道选择列表 m_width_controls.Show(aLeft+class="num">10,m_y+class="num">60); class=class="str">"cmt">// 通道参数控件群 m_y=m_width_controls.GetLastY(); class=class="str">"cmt">// 得到最后控件的坐标 m_w_value2.Show(aLeft+class="num">10,m_y+class="num">20); class=class="str">"cmt">// 宽度输入字段 m_w_shift.Show(aLeft+class="num">10,m_y+class="num">40); class=class="str">"cmt">// 平移参数输入字段 } class="type">void OnHideEvent(){ m_c_cmb_main.Hide(); class=class="str">"cmt">// 中心线类型选择列表 m_central_controls.Hide(); class=class="str">"cmt">// 中心线参数控件群 m_c_shift.Hide(); class=class="str">"cmt">// 平移参数输入字段 m_w_cmb_main.Hide(); class=class="str">"cmt">// 通道选择字段 m_width_controls.Hide(); class=class="str">"cmt">// 通道参数控件群 m_w_shift.Hide(); class=class="str">"cmt">// 平移参数输入字段 m_w_lockto.Hide(); class=class="str">"cmt">// 周期锁定类型选择 m_width_controls.Hide(); class=class="str">"cmt">// 宽度输入字段 } class="type">void SetValues(class="type">int c_value1, class="type">int c_value2, class="type">int c_value3, class="type">long c_method, class="type">long c_price, class="type">long c_shift, class="type">int w_value1, class="type">int w_value2, class="type">long w_method, class="type">long w_price, class="type">long w_lockto, class="type">long w_shift ){ class=class="str">"cmt">// 中心线输入字段 m_c_value1.SetValue(c_value1); m_c_value2.SetValue(c_value2); m_c_value3.SetValue(c_value3); m_c_shift.SetValue(c_shift); class=class="str">"cmt">// 通道参数输入字段
◍ GUI 状态回填与中枢类型动态重建
面板在初始化或参数变更时,需要把后台变量同步到前端控件。代码先通过 SetValue 把通道宽度、偏移等数值写回对应编辑框,保证界面显示和逻辑变量一致。 接着用三组 for 循环遍历枚举数组,将当前选定的平滑方法、价格类型、通道周期锁定方式,映射到下拉框的选中索引。注意平滑方法和价格类型循环未写 break,而锁定类型在命中后立即 break,说明前者可能有多控件需同步、后者为单选独占。 SetCentralType 负责按类型重建中枢控件对象。若旧对象已存在(CheckPointer 返回 POINTER_DYNAMIC),先 delete 释放再置 NULL,避免内存泄漏;随后 switch 根据 UniCh_C_AMA / DEMA / FrAMA / MA / TEMA / VIDyA / PrCh 等 7 种枚举 new 出对应子类。 新对象拿到输入控件指针后调用 InitControls 完成初始化,并把数值框设为可写(SetReadOnly(false)),这样交易者能直接键入参数。在 MT5 里跑这套,重点看 delete 是否每次切换类型都触发,漏掉会在多次切换后拖慢终端。外汇与贵金属波动剧烈,参数误填可能引发信号漂移,实操前先用模拟盘验证。
m_w_value1.SetValue(w_value1); m_w_value2.SetValue(w_value2); m_w_shift.SetValue(w_shift); class=class="str">"cmt">// 显示在平滑方法选择列表中所选的类型 for(class="type">int i=class="num">0;i<ArraySize(e_method);i++){ if(c_method==e_method[i]){ m_c_method.SetSelectedIndex(i); } if(w_method==e_method[i]){ m_w_method.SetSelectedIndex(i); } } class=class="str">"cmt">// 显示在价格类型选择列表中所选的类型 for(class="type">int i=class="num">0;i<ArraySize(e_price);i++){ if(c_price==e_price[i]){ m_c_price.SetSelectedIndex(i); } if(w_price==e_price[i]){ m_w_price.SetSelectedIndex(i); } } class=class="str">"cmt">// 显示所选的通道周期锁定类型 for(class="type">int i=class="num">0;i<ArraySize(e_lockto);i++){ if(w_lockto==e_lockto[i]){ m_w_lockto.SetSelectedIndex(i); break; } } } class="type">void SetCentralType(class="type">long type){ class=class="str">"cmt">// 如果一个对象已经被创建, 我们需要删除它 if(CheckPointer(m_central_controls)==POINTER_DYNAMIC){ class="kw">delete(m_central_controls); m_central_controls=NULL; } class="kw">switch((ECType)type){ class=class="str">"cmt">// 根据所选的类创建一个对象 case UniCh_C_AMA: m_central_controls=new CUniChannelCentralControls_AMA(); break; case UniCh_C_DEMA: m_central_controls=new CUniChannelCentralControls_DEMA(); break; case UniCh_C_FrAMA: m_central_controls=new CUniChannelCentralControls_FrAMA(); break; case UniCh_C_MA: m_central_controls=new CUniChannelCentralControls_MA(); break; case UniCh_C_TEMA: m_central_controls=new CUniChannelCentralControls_TEMA(); break; case UniCh_C_VIDyA: m_central_controls=new CUniChannelCentralControls_VIDyA(); break; case UniCh_C_PrCh: m_central_controls=new CUniChannelCentralControls_PrCh(); break; } class=class="str">"cmt">// 将指针传递给控件对象 m_central_controls.SetPointers(m_c_value1,m_c_value2,m_c_value3,m_c_price,m_c_method); class=class="str">"cmt">// 控件群的初始化 m_central_controls.InitControls(); class=class="str">"cmt">// 允许从键盘输入数值 m_c_value1.SetReadOnly(false); m_c_value2.SetReadOnly(false);
通道参数面板的宽度分支与高度自算
在自定义通道指标的设置面板里,中心线三组参数(m_c_value1~3)先解除只读并统一把最小允许值锁在 1,避免交易者手滑填出 0 或负数导致通道崩算。随后调用 this.SolveHeight() 按控件数量重算表单高度,这一步决定了面板在 MT5 左侧是否会被截断。 宽度控制对象的创建走的是先删后建的逻辑:若 m_width_controls 已是动态指针,先 delete 置 NULL,再根据传入的 type 分支 new 出 ATR、StdDev、Points、Percents、PrCh 五类子控件。也就是说,切换通道宽度算法时旧对象会被干净回收,不会泄漏句柄。 宽度侧的两个输入框最小约束不一样——m_w_value1 最小为 1,m_w_value2 最小为 0,这对应部分算法(如点数或百分比)允许第二参数缺省为 0。最后同样走 InitControls() 与 SolveHeight() 收尾。 SolveHeight 本身只做一件事:当两个控件群指针都有效时,高度 =(宽度控件数 + 中心线控件数 + 6)× 20 + 10 像素。按这个算式,若两类控件各挂 5 个,面板高度会是 (5+5+6)*20+10 = 270 像素,开 MT5 把面板拉出来数一下行距就能验证是否一致。
m_c_value3.SetReadOnly(false); class=class="str">"cmt">// 设置最小允许值 m_c_value1.SetMinValue(class="num">1); m_c_value2.SetMinValue(class="num">1); m_c_value3.SetMinValue(class="num">1); class=class="str">"cmt">// 计算表单的高度 this.SolveHeight(); } class="type">void SetWidthType(class="type">long type){ class=class="str">"cmt">// 如果一个对象已经被创建, 我们需要删除它 if(CheckPointer(m_width_controls)==POINTER_DYNAMIC){ class="kw">delete(m_width_controls); m_width_controls=NULL; } class="kw">switch((EWType)type){ class=class="str">"cmt">// 根据所选的类创建一个对象 case UniCh_W_ATR: m_width_controls=new CUniChannelWidthControls_ATR(); break; case UniCh_W_StdDev: m_width_controls=new CUniChannelWidthControls_StdDev(); break; case UniCh_W_Points: m_width_controls=new CUniChannelWidthControls_InPoints(); break; case UniCh_W_Percents: m_width_controls=new CUniChannelWidthControls_Envelopes(); break; case UniCh_W_PrCh: m_width_controls=new CUniChannelWidthControls_PrCh(); break; } class=class="str">"cmt">// 将指针传递给控件对象 m_width_controls.SetPointers(m_w_value1,m_w_value2,m_w_price,m_w_method); class=class="str">"cmt">// 控件群初始化 m_width_controls.InitControls(); class=class="str">"cmt">// 设置最小允许值 m_w_value1.SetReadOnly(false); m_w_value2.SetReadOnly(false); class=class="str">"cmt">// 设置最小允许值 m_w_value1.SetMinValue(class="num">1); m_w_value2.SetMinValue(class="num">0); class=class="str">"cmt">// 计算表单的高度 this.SolveHeight(); } class="type">void SolveHeight(){ class=class="str">"cmt">// 如果两个对象均存在 (中心线和宽度) if(CheckPointer(m_central_controls)==POINTER_DYNAMIC && CheckPointer(m_width_controls)==POINTER_DYNAMIC){ m_Height=(m_width_controls.ControlsCount()+m_central_controls.ControlsCount()+class="num">6)*class="num">20+class="num">10; } }
「给通用通道挂上可交互控件」
把 iUniChannel 存成 iUniChannelGUI 后,先在属性窗口顶层暴露一个 UseGUI 开关,再把 UseDefault 和 KeepPrev 两个布尔量默认置 true 并可见。这样做的好处是:不点开 GUI 时指标行为和原版一致,老图表加载不会崩。 类文件通过 #include <UniChannel/UniChannelGUI.mqh> 引入,OnInit() 末尾才真正实例化表单。这里要先备好中心线类型数组 ctype[] 与边界类型数组 wtype[],再声明 CUniChannelForm * frm 指针,否则编译会报未定义。 表单创建后,指标选择列表会被填充,控件数值也同步写入。指标一挂图,带十字关闭键的窗体就弹出来。OnChartEvent() 里分了六个事件,复杂逻辑拆到 EventForm / EventCentralTypeChange / EventWidthTypeChange / EventCentralParametersChange / EventWidthParametersChange 等独立函数里。 一个实测踩坑:UseDefault=true 且开了周期锁定时,锁会失效。根因是第二个宽度指标在构造时改了参数。修法是给 CChannelUni_Calculate_ATR、StdDev、PriceChannel 的构造函数加可选 locked 参数,默认 false;locked=true 时 ma_period 不被默认值覆盖。 跨周期参数保存也接上了。OnDeinit() 里若因切周期触发,就把当前参数写成隐藏图形对象;OnInit() 调 PrepareParameters() 之后跑 LoadSavedParameters(),有存档就返回 true,此时 LoadCentral() 和 LoadWidth() 传 false 避免被默认覆盖。外汇与贵金属波动剧烈,这类 GUI 指标仅辅助判断,不构成方向保证,实盘前请在 MT5 策略测试器跑一遍。
class="kw">input class="type">bool UseGUI = true; class="kw">input class="type">bool UseDefault = true; class="kw">input class="type">bool KeepPrev = true; class=class="str">"cmt">// 中心线类型的数组 ECType ctype[]={ UniCh_C_AMA, UniCh_C_DEMA, UniCh_C_FrAMA, UniCh_C_MA, UniCh_C_TEMA, UniCh_C_VIDyA, UniCh_C_PrCh }; class=class="str">"cmt">// 边界类型的数组 EWType wtype[]={ UniCh_W_ATR, UniCh_W_StdDev, UniCh_W_Points, UniCh_W_Percents, UniCh_W_PrCh }; if(UseGUI){ class=class="str">"cmt">// 表单对象的创建和初始化 frm=new CUniChannelForm(); frm.Init(); class=class="str">"cmt">// 辅助变量 class="type">int ind1=class="num">0; class="type">int ind2=class="num">0; class=class="str">"cmt">// 在中心线类型数组中搜索所选类型的中心线 for(class="type">int i=class="num">0;i<ArraySize(ctype);i++){ frm.m_c_cmb_main.AddItem(EnumToString(ctype[i])); if(ctype[i]==_CentralType){ ind1=i; } } class=class="str">"cmt">// 在边界类型数组中搜索所选类型的通道边界 for(class="type">int i=class="num">0;i<ArraySize(wtype);i++){ frm.m_w_cmb_main.AddItem(EnumToString(wtype[i])); if(wtype[i]==_WidthType){ ind2=i; } } class=class="str">"cmt">// 显示列表中所选的中心线类型 frm.m_c_cmb_main.SetSelectedIndex(ind1); class=class="str">"cmt">// 准备与类型相对应的控件 frm.SetCentralType(_CentralType); class=class="str">"cmt">// 显示在列表中所选的边界类型 frm.m_w_cmb_main.SetSelectedIndex(ind2); frm.SetWidthType(_WidthType); class=class="str">"cmt">// 设置数值 frm.SetValues( _c_Period1,
◍ 图表事件如何驱动通道重绘
这段逻辑把用户在点价通道表单上的每一次操作,都映射成具体对象的销毁与重建。OnChartEvent 里分了五类判断:表单自身、中心线类型、边界类型、中心参数、边界参数,以及平移参数,任一返回有效事件就调对应的 Event 函数。 中心线类型切换时,先按下拉索引取出 _CentralType,delete 掉旧的 central 对象再 LoadCentral(true) 重载。若 central.CheckHandle() 为假,弹 Alert 报出具体名称,避免静默失效。 EventForm 则用 ChartWindowFind(0,ShortName) 定位子窗口,ChartIndicatorDelete 删指标后 ChartRedraw 强制重绘。外汇与贵金属行情跳空频繁,这类手动改参数触发的重绘在高波动时段可能滞后 1~2 个 tick,验证时建议开 EURUSD 的 M5 连点下拉看重绘延迟。
_c_Period2, _c_Period3, _c_Method, _c_Price, _c_Shift, _w_Period, _w_Width, _w_Method, _w_Price, _w_LockPeriod, _w_Shift ); class=class="str">"cmt">// 设置表单属性 frm.SetSubWindow(class="num">0); frm.SetPos(class="num">10,class="num">30); class=class="str">"cmt">// 显示表单 frm.Show(); } class="type">void OnChartEvent(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=class="str">"cmt">// 表单事件 if(frm.Event(id,lparam,dparam,sparam)==class="num">1){ EventForm(); } class=class="str">"cmt">// 选择中心线类型 if(frm.m_c_cmb_main.Event(id,lparam,dparam,sparam)==class="num">1){ EventCentralTypeChange(); } class=class="str">"cmt">// 选择边界类型 if(frm.m_w_cmb_main.Event(id,lparam,dparam,sparam)==class="num">1){ EventWidthTypeChange(); } class=class="str">"cmt">// 改变中心线参数 if(frm.m_central_controls.Event(id,lparam,dparam,sparam)==class="num">1){ EventCentralParametersChange(); } class=class="str">"cmt">// 改变边界参数 if(frm.m_width_controls.Event(id,lparam,dparam,sparam)==class="num">1){ EventWidthParametersChange(); } class=class="str">"cmt">// 改变平移参数 if(frm.m_c_shift.Event(id,lparam,dparam,sparam)!=class="num">0 || frm.m_w_shift.Event(id,lparam,dparam,sparam) ){ EventShift(); } } class="type">void EventForm(){ class="type">int win=ChartWindowFind(class="num">0,ShortName); class=class="str">"cmt">// 判断指标子窗口 ChartIndicatorDelete(class="num">0,win,ShortName);class=class="str">"cmt">// 删除指标 ChartRedraw(); } class="type">void EventCentralTypeChange(){ class=class="str">"cmt">// 得到的新类型保存到变量 _CentralType=ctype[frm.m_c_cmb_main.SelectedIndex()]; class=class="str">"cmt">// 删除旧对象并创建一个新的 class="kw">delete(central); LoadCentral(true); class=class="str">"cmt">// 检查指标加载 if(!central.CheckHandle()){ Alert("当指标加载时错误 "+central.Name()); } class=class="str">"cmt">// 设置偏移量和缓冲区名称 SetStyles(); class=class="str">"cmt">// 在列表中设置新类型 frm.SetCentralType(ctype[frm.m_c_cmb_main.SelectedIndex()]); class=class="str">"cmt">// 在表单上更新参数值 frm.SetValues( _c_Period1, _c_Period2, _c_Period3,
参数联动下的指标热重载
在 MT5 自定义指标面板里,周期1/2/3 的输入框(m_c_value1~3)一旦被交易者改成正值,就会回写对应的 _c_Period 变量。若宽度指标勾选了锁定到某一周期(_w_LockPeriod 等于 LockTo_Period1/2/3),则 _w_Period 会同步成该值,并通过 frm.m_w_value1.SetValue 刷新界面,同时把 dolock 置真,意味宽度指标需要重建。 EventSetMillisecondTimer(100) 这行决定面板操作后最多 100 毫秒触发一次重算,属于偏激进的轮询频率,CPU 占用会明显高于 250~500 毫秒的常规设置,实盘跑黄金或外汇时建议自己压到 200 以上观察负载。 EventCentralParametersChange 里真正的硬动作是 delete(central) 后 LoadCentral(false) 以及条件性的 delete(width) 后 LoadWidth(false)——旧句柄不释放就新建会漏内存。重建完用 CheckHandle 判空,失败直接 Alert 弹“Error while loading indicator”加指标名,这一步不能省。 方法(_c_Method)与价格(_c_Price)的切换走下拉框 SelectedIndex,映射自 e_method / e_price 枚举;改完统一调 SetStyles 重绑偏移和缓冲区名,否则画线可能错位。
_c_Method, _c_Price, _c_Shift, _w_Period, _w_Width, _w_Method, _w_Price, _w_LockPeriod, _w_Shift ); class=class="str">"cmt">// 刷新表单 frm.Refresh(); class=class="str">"cmt">// 启动指标重计算的定时器 EventSetMillisecondTimer(class="num">100); } class="type">void EventCentralParametersChange(){ class=class="str">"cmt">// 该变量表示需要重新启动边界指标 class="type">bool dolock=false; class=class="str">"cmt">// 修改周期 class="num">1 的数值 if((class="type">int)frm.m_c_value1.Value()>class="num">0){ class=class="str">"cmt">// 将从控件接收的值分配给变量 _c_Period1=(class="type">int)frm.m_c_value1.Value(); class=class="str">"cmt">// 如果周期 class="num">1 与宽度指标的周期相连接 if(_w_LockPeriod==LockTo_Period1){ class=class="str">"cmt">// 我们用宽度指标周期设置为周期 class="num">1 的值 _w_Period=_c_Period1; class=class="str">"cmt">// 在表单上显示它 frm.m_w_value1.SetValue(_w_Period); class=class="str">"cmt">// 表示第二个指标需要重启 dolock=true; } } class=class="str">"cmt">// 修改周期 class="num">2 的数值类似于周期 class="num">1 if((class="type">int)frm.m_c_value2.Value()>class="num">0){ _c_Period2=(class="type">int)frm.m_c_value2.Value(); if(_w_LockPeriod==LockTo_Period2){ _w_Period=_c_Period2; frm.m_w_value1.SetValue(_w_Period); dolock=true; } } class=class="str">"cmt">// 修改周期 class="num">3 的数值类似于周期 class="num">1 if((class="type">int)frm.m_c_value3.Value()>class="num">0){ _c_Period3=(class="type">int)frm.m_c_value3.Value(); if(_w_LockPeriod==LockTo_Period3){ _w_Period=_c_Period3; frm.m_w_value1.SetValue(_w_Period); dolock=true; } } class=class="str">"cmt">// 修改方法 if(frm.m_c_method.SelectedIndex()!=-class="num">1){ _c_Method=e_method[frm.m_c_method.SelectedIndex()]; } class=class="str">"cmt">// 修改价格 if(frm.m_c_price.SelectedIndex()!=-class="num">1){ _c_Price=e_price[frm.m_c_price.SelectedIndex()]; } class=class="str">"cmt">// 删除旧的对象并创建新的 class="kw">delete(central); LoadCentral(false); if(!central.CheckHandle()){ Alert("Error class="kw">while loading indicator "+central.Name()); } class=class="str">"cmt">// 删除第二个指标的对象并创建新的 if(dolock){ class="kw">delete(width); LoadWidth(false); if(!width.CheckHandle()){ Alert("当加载指标时错误 "+width.Name()); } } class=class="str">"cmt">// 设置偏移量和缓冲区名称 SetStyles(); class=class="str">"cmt">// 启动指标重计算定时器
「正文」
<span class="functions">EventSetMillisecondTimer</span>(<span class="number">100</span>); } <span class="keyword">void</span> EventWidthParametersChange(){ <span class="comment">// 该变量表示需要重新启动中心线指标</span> <span class="keyword">bool</span> dolock=<span class="macro">false</span>; <span class="comment">// 修改周期</span> <span class="keyword">if</span>((<span class="keyword">int</span>)frm.m_w_value1.Value()><span class="number">0</span>){ <span class="comment">// 将接收自控件的数值分配给变量</span> _w_Period=(<span class="keyword">int</span>)frm.m_w_value1.Value(); <span class="comment">// 进行锁定</span> <span class="comment">// 宽度参数与中心线的第一个周期相连接</span> <span class="keyword">if</span>(_w_LockPeriod==LockTo_Period1){ <span class="comment">// 为中心线指标的变量分配新值 </span> _c_Period1=_w_Period; <span class="comment">// 在表单上更新数值</span> frm.m_c_value1.SetValue(_c_Period1); <span class="comment">// 表示需要重启宽度指标</span> dol
◍ 参数持久化与图表事件清理
指标在图表事件触发时需要决定是留存还是清除界面参数对象。若回调原因不是 REASON_CHARTCHANGE,代码会连续删除 15 个以 _c_ 和 _w_ 前缀命名的图形对象,避免旧参数标签残留在切换周期后的图上。 当 reason 等于 REASON_CHARTCHANGE 时,逻辑反过来:把 _CentralType、_c_Period1~3、_c_Shift、_c_Method、_c_Price 以及 _WidthType、_w_Period、_w_LockPeriod、_w_Shift、_w_Method、_w_Price、_w_Width 共 15 个变量通过 SaveParameter 写回图形对象,实现参数随图表刷新而保留。 EventSetMillisecondTimer(100) 设定了 100 毫秒的毫秒级定时器,配合 EventShift 从窗体控件读 m_c_shift、m_w_shift 的值赋给 _c_Shift、_w_Shift,再调 SetStyles 与 ChartRedraw 实时重绘。外汇与贵金属品种波动剧烈,这类高频重绘可能增加终端负载,建议在模拟盘先验证计时器间隔是否合适。 use_default 分支里,keep_previous 为真且 ma_period 为 -1、未锁定时才回退到 14,ch_width 回退到 2;否则直接强制 ma_period=14、ch_width=2。这个 -1 哨兵值和外置 locked 标志的组合,决定了用户手动改过的参数会不会被默认逻辑覆盖。
EventSetMillisecondTimer(class="num">100); } class="type">void EventShift(){ class=class="str">"cmt">// 在变量中接收新值 _c_Shift=(class="type">int)frm.m_c_shift.Value(); _w_Shift=(class="type">int)frm.m_w_shift.Value(); class=class="str">"cmt">// 设置新样式 SetStyles(); class=class="str">"cmt">// 刷新图表 ChartRedraw(); } if(use_default){ if(keep_previous){ if(ma_period==-class="num">1 && !locked)ma_period=class="num">14; class=class="str">"cmt">// 改变 if(ch_width==-class="num">1)ch_width=class="num">2; } else{ if(!locked)ma_period=class="num">14; class=class="str">"cmt">// 改变 ch_width=class="num">2; } } class="type">void SaveOrDeleteParameters(const class="type">int reason){ class=class="str">"cmt">// 如果不是图表改变, 我们应该删除图形对象 if(reason!=REASON_CHARTCHANGE){ ObjectDelete(class="num">0,"_CentralType"); ObjectDelete(class="num">0,"_c_Period1"); ObjectDelete(class="num">0,"_c_Period2"); ObjectDelete(class="num">0,"_c_Period3"); ObjectDelete(class="num">0,"_c_Shift"); ObjectDelete(class="num">0,"_c_Method"); ObjectDelete(class="num">0,"_c_Price"); ObjectDelete(class="num">0,"_WidthType"); ObjectDelete(class="num">0,"_w_Period"); ObjectDelete(class="num">0,"_w_LockPeriod"); ObjectDelete(class="num">0,"_w_Shift"); ObjectDelete(class="num">0,"_w_Method"); ObjectDelete(class="num">0,"_w_Price"); ObjectDelete(class="num">0,"_w_Width"); } else{ class=class="str">"cmt">// 在改变图表时, 我们使用参数值创建图形对象 SaveParameter("_CentralType",(class="type">class="kw">string)_CentralType); SaveParameter("_c_Period1",(class="type">class="kw">string)_c_Period1); SaveParameter("_c_Period2",(class="type">class="kw">string)_c_Period2); SaveParameter("_c_Period3",(class="type">class="kw">string)_c_Period3); SaveParameter("_c_Shift",(class="type">class="kw">string)_c_Shift); SaveParameter("_c_Method",(class="type">class="kw">string)_c_Method); SaveParameter("_c_Price",(class="type">class="kw">string)_c_Price); SaveParameter("_WidthType",(class="type">class="kw">string)_WidthType); SaveParameter("_w_Period",(class="type">class="kw">string)_w_Period); SaveParameter("_w_LockPeriod",(class="type">class="kw">string)_w_LockPeriod); SaveParameter("_w_Shift",(class="type">class="kw">string)_w_Shift); SaveParameter("_w_Method",(class="type">class="kw">string)_w_Method); SaveParameter("_w_Price",(class="type">class="kw">string)_w_Price);
用隐藏标签对象记住你的画线参数
在 MT5 里做自定义指标或脚本时,最烦的是每次重开图表都得重新填一遍中枢类型、周期、偏移这些参数。上面这段逻辑的思路是:把参数写进图表上的 OBJ_LABEL 文本对象,名字用下划线前缀(如 _w_Width)做标记,下次加载时直接读对象文本就能还原。 SaveParameter 先查 ObjectFind,若返回 -1 说明对象不存在,就用 ObjectCreate 建一个 OBJ_LABEL,并把 XDISTANCE 设 0、YDISTANCE 设 -30 让它躲到图表左上角看不见的地方;随后 ObjectSetString 把 value 写进 OBJPROP_TEXT。这样 14 个参数对象(_CentralType 到 _w_Width)就常驻图表了。 LoadSavedParameters 用一连串 ObjectFind==0 判断这 14 个对象是否都在,全在才继续;随后逐个 ObjectGetString 读文本并强转类型,比如 _c_Period1 转 int、_c_Method 转 long。外汇与贵金属波动剧烈、杠杆高风险,参数误读可能导致信号偏移,建议你在策略测试器里先跑一遍 EURUSD 的 M15 验证对象读写是否对齐。 别把标签当垃圾 这些 OBJ_LABEL 不会自动随指标删除,若你改了参数命名却没清旧对象,LoadSavedParameters 可能读到过期值。手动在 MT5 对象列表里删掉带下划线的标签再重载,能避开这类幽灵参数。
SaveParameter("_w_Width",(class="type">class="kw">string)_w_Width); } } class=class="str">"cmt">// 保存图形对象中一个参数的辅助函数 class="type">void SaveParameter(class="type">class="kw">string name,class="type">class="kw">string value){ if(ObjectFind(class="num">0,name)==-class="num">1){ ObjectCreate(class="num">0,name,OBJ_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,name,OBJPROP_XDISTANCE,class="num">0); ObjectSetInteger(class="num">0,name,OBJPROP_YDISTANCE,-class="num">30); } ObjectSetString(class="num">0,name,OBJPROP_TEXT,value); } class="type">bool LoadSavedParameters(){ class=class="str">"cmt">// 如果所有带参数的对象存在 if(ObjectFind(class="num">0,"_CentralType")==class="num">0 && ObjectFind(class="num">0,"_c_Period1")==class="num">0 && ObjectFind(class="num">0,"_c_Period2")==class="num">0 && ObjectFind(class="num">0,"_c_Period3")==class="num">0 && ObjectFind(class="num">0,"_c_Shift")==class="num">0 && ObjectFind(class="num">0,"_c_Method")==class="num">0 && ObjectFind(class="num">0,"_c_Price")==class="num">0 && ObjectFind(class="num">0,"_WidthType")==class="num">0 && ObjectFind(class="num">0,"_w_Period")==class="num">0 && ObjectFind(class="num">0,"_w_LockPeriod")==class="num">0 && ObjectFind(class="num">0,"_w_Shift")==class="num">0 && ObjectFind(class="num">0,"_w_Method")==class="num">0 && ObjectFind(class="num">0,"_w_Price")==class="num">0 && ObjectFind(class="num">0,"_w_Width")==class="num">0 ){ class=class="str">"cmt">// 从图形对象中获取数值 _CentralType=(ECType)ObjectGetString(class="num">0,"_CentralType",OBJPROP_TEXT); _c_Period1=(class="type">int)ObjectGetString(class="num">0,"_c_Period1",OBJPROP_TEXT); _c_Period2=(class="type">int)ObjectGetString(class="num">0,"_c_Period2",OBJPROP_TEXT); _c_Period3=(class="type">int)ObjectGetString(class="num">0,"_c_Period3",OBJPROP_TEXT); _c_Shift=(class="type">int)ObjectGetString(class="num">0,"_c_Shift",OBJPROP_TEXT); _c_Method=(class="type">long)ObjectGetString(class="num">0,"_c_Method",OBJPROP_TEXT); _c_Price=(class="type">long)ObjectGetString(class="num">0,"_c_Price",OBJPROP_TEXT);
「从对象属性回读通道参数」
这段逻辑把画在图表上的文本对象属性逐个读回变量,用来恢复之前保存的通道设置。它依赖 ObjectGetString 在 OBJPROP_TEXT 上取字符串,再强转成对应的枚举、整型或双精度,因此对象名必须与下方字符串字面量完全一致,否则读出来是空串。 _WidthType 取的是 EWType 枚举,_w_Period 和 _w_Shift 转 int,_w_LockPeriod、_w_Method、_w_Price 转 long,_w_Width 转 double——七个字段缺一不可。若读取成功函数返回 true,失败则返回 false,外层用 ChartCange 接收后决定要不要 LoadCentral 重画。 在 MT5 里验证时,可先手动建一个文本对象命名为 _w_Period 并写 "100",再跑这段看 _w_Period 是否变成 100;外汇与贵金属图表上做这类对象交互存在报价跳空导致对象丢失的高风险,建议先在回测环境试。
_WidthType=(EWType)ObjectGetString(class="num">0,"_WidthType",OBJPROP_TEXT); _w_Period=(class="type">int)ObjectGetString(class="num">0,"_w_Period",OBJPROP_TEXT); _w_LockPeriod=(class="type">long)ObjectGetString(class="num">0,"_w_LockPeriod",OBJPROP_TEXT); _w_Shift=(class="type">int)ObjectGetString(class="num">0,"_w_Shift",OBJPROP_TEXT); _w_Method=(class="type">long)ObjectGetString(class="num">0,"_w_Method",OBJPROP_TEXT); _w_Price=(class="type">long)ObjectGetString(class="num">0,"_w_Price",OBJPROP_TEXT); _w_Width=(class="type">class="kw">double)ObjectGetString(class="num">0,"_w_Width",OBJPROP_TEXT); class="kw">return(true); } else{ class="kw">return(false); } } class="type">bool ChartCange=LoadSavedParameters(); LoadCentral(!ChartCange);
◍ 通用通道的工程代价与组合弹性
通用通道相比通用振荡器,工作量几乎翻倍还不止——核心难点在于它拆成了两个独立单元:中心线和通道边界,各自要单独算。再加上和周期锁定函数绑定的参数变化算法更复杂,新指标加载时得同时挂两个指标,初始化逻辑明显更重。 我们还顺手加了一个实用特性:切换时间帧时保留参数,不用反复重设。代价是代码体量上去了,但换来的是通道思路的扩展性——中心线算法和边界构建方法可以分开挑,组合空间一下子铺开。 在 MT5 里把这两个单元拆开试不同搭配,能直观看出哪组边界对贵金属 4H 级别的假突破过滤更有效;外汇品种波动更碎,建议先拿 EURUSD 的 1H 跑一遍验证。高杠杆品种务必小仓位试探,通道类指标在单边市会连续挨扫。 图形界面把组合切换速度拉起来了,不然几十种搭配靠手填参数根本没法做视觉比对。
随包文件与放置要点
这篇系列文把用到的脚本、指标和测试工程打进了一个 93 KB 的 ZIP 包,下载后直接解到 MT5 终端对应的数据目录里,和原 mq5 文件同目录存放,编译调用才不会报找不到依赖。 包里不含名为 iPriceChannel 的标识文件,2019 年有用户在评论区提过这个缺失,若你的策略依赖该通道指标,得自行补一个同名自定义指标再跑示例。 外汇与贵金属品种波动受杠杆和消息面影响大,这类示例工程只解决「能跑通」的问题,实盘前务必在策略测试器里用历史数据复核逻辑。