带有图形用户界面的通用震荡指标·进阶篇
🧩

带有图形用户界面的通用震荡指标·进阶篇

(2/3)· 从面向对象基类到表单控件,把 10+ 震荡指标塞进一个带界面的壳里

新手友好 第 2/3 篇
在图表上反复拖指标、改参数对比效果,一天能耗掉半桶精力。给通用指标套一层图形界面,切品种改周期当场出图,比走属性窗口快一个量级。

◍ 给统一震荡指标搭一个基类骨架

在 MT5 的 Include 目录下新建 UniOsc 文件夹,后续所有扩展震荡指标文件都归到这里。先写一份 UniOscDefines.mqh,只放一个枚举 EOscUnyType,把 ATR、BearsPower、BullsPower、CCI、Chaikin、DeMarker、Force、Momentum、MACD、OsMA、RSI、RVI、Stochastic、TriX、WPR 这 15 种震荡类型列清楚,别处也要复用,所以单独成文件最干净。 真正的基类放在 CUniOsc.mqh 里,类名 COscUni。protected 区留作子类可访问的成员,private 不放东西,避免子类拿不到句柄和缓冲区配置。核心方法是 Calculate(),对应指标的 OnCalculate(),只接 rates_total 和 prev_calculated 两个 int,再加 buffer0[]、buffer1[] 两个双精度引用——不传原始价格数组,因为数据走其他指标缓冲区。哪怕某指标只用一条缓冲区,也强制传两条,第二条留作控制。 Calculate() 声明为虚函数,因为不同震荡指标可能用 1 个或 2 个缓冲区,具体逻辑推迟到子类实现。基类先返回 rates_total 占位。 m_handle 放在 protected,记录指标句柄;另有一组变量管显示:m_bufferscnt、m_name、m_label1/2、m_drawtype1/2、m_help、m_digits、m_levels_total 和 m_level_value[]。这些在子类加载具体指标时赋值。CheckHandle() 用 m_handle!=INVALID_HANDLE 判断加载是否成功;Handle() 直接返回句柄,供 BarsCalculated() 查计算进度,图形界面切指标时能知道是不是算完了。 构造里把 m_handle 置为 INVALID_HANDLE,析构里若句柄有效就调 IndicatorRelease() 释放,防止内存泄漏。剩下的 Name()、BuffersCount()、Label1()、Label2()、DrawType1/2()、Help()、Digits()、LevelsTotal()、LevelValue() 全是只读 getter,返回对应变量,值由子类填。 开 MT5 照这个结构建好两个文件,编译能过就说明基类骨架立住了,接下来往里塞具体指标的子类即可。外汇与贵金属指标开发涉及实盘逻辑,回测与加载失败均属高风险操作,需自行验证。

MQL5 / C++
enum EOscUnyType{
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

class COscUni{
   class="kw">protected:
   class="kw">public:
};

class="kw">virtual class="type">int Calculate( class="kw">const class="type">int rates_total,
               class="kw">const class="type">int prev_calculated,
               class="type">class="kw">double & buffer0[],
               class="type">class="kw">double & buffer1[] ){
   class="kw">return (rates_total);
}

class="type">int m_handle;           class=class="str">"cmt">// 指标句柄
class="type">int m_bufferscnt;       class=class="str">"cmt">// 使用的缓冲区数量
class="type">class="kw">string m_name;          class=class="str">"cmt">// 指标名称
class="type">class="kw">string m_label1;        class=class="str">"cmt">// 缓冲区1的名称
class="type">class="kw">string m_label2;        class=class="str">"cmt">// 缓冲区2的名称
class="type">int m_drawtype1;        class=class="str">"cmt">// 缓冲区1的绘图类型
class="type">int m_drawtype2;        class=class="str">"cmt">// 缓冲区2的绘图类型
class="type">class="kw">string m_help;          class=class="str">"cmt">// 指标参数的提示
class="type">int m_digits;           class=class="str">"cmt">// 指标值中小数点位数
class="type">int m_levels_total;     class=class="str">"cmt">// 水平总数
class="type">class="kw">double m_level_value[]; class=class="str">"cmt">// 水平数值的数组

class="type">bool CheckHandle(){
   class="kw">return (m_handle!= INVALID_HANDLE );
}

class="type">int Handle(){
   class="kw">return (m_handle);
}

class="type">void COscUni(){
   m_handle= INVALID_HANDLE ;
}
class="type">void ~COscUni(){
   if (m_handle!= INVALID_HANDLE ){
       IndicatorRelease(m_handle);
   }
}

class="type">class="kw">string Name(){ class=class="str">"cmt">// 震荡指标的名称
   class="kw">return (m_name);
}
class="type">int BuffersCount(){ class=class="str">"cmt">// 震荡指标缓冲区的数量
   class="kw">return (m_bufferscnt);
}
class="type">class="kw">string Label1(){ class=class="str">"cmt">// 第一个缓冲区的名称
   class="kw">return (m_label1);
}
class="type">class="kw">string Label2(){ class=class="str">"cmt">// 第二个缓冲区的名称
   class="kw">return (m_label2);
}
class="type">int DrawType1(){ class=class="str">"cmt">// 第一个缓冲区的绘图类型
   class="kw">return (m_drawtype1);
}
class="type">int DrawType2(){ class=class="str">"cmt">// 第二个缓冲区的绘图类型
   class="kw">return (m_drawtype2);
}
class="type">class="kw">string Help(){ class=class="str">"cmt">// 使用参数的提示
   class="kw">return (m_help);
}
class="type">int Digits(){ class=class="str">"cmt">// 指标值的小数点位数
   class="kw">return (m_digits);
}
class="type">int LevelsTotal(){ class=class="str">"cmt">// 指标水平的数量
   class="kw">return (m_levels_total);
}
class="type">class="kw">double LevelValue( class="type">int index){ class=class="str">"cmt">// 根据指定的索引取得水平的数值
   class="kw">return (m_level_value[index]);
}

单缓冲与双缓冲指标的计算子类

把通用振荡器再往下拆一层,会落到两个具体子类:只吃一个缓冲区的 COscUni_Calculate1,以及吃两个缓冲区的 COscUni_Calculate2。前者在构造函数里把 m_bufferscnt 写死为 1,后者写死为 2,缓冲区个数在对象生成时就定调,后面 Calculate 不再改。 COscUni_Calculate1 的 Calculate 方法先根据 prev_calculated 算 cnt 和 start:首次跑(prev_calculated==0)时 cnt=rates_total、start=0;后续刷新时 cnt=rates_total-prev_calculated+1、start=prev_calculated-1。随后用 CopyBuffer 把指标句柄的第 0 号缓冲拉进 buffer0,失败直接 return(0),迫使下一 tick 重算。空出来的 buffer1 从 start 到 rates_total 全填 EMPTY_VALUE,最后回 rates_total。 双缓冲版本更直白:cnt 算法完全一致,但连续两次 CopyBuffer 分别把 0 号和 1 号缓冲搬进 buffer0、buffer1,任一次失败就 return(0)。没有清空辅助缓冲的动作,因为两个缓冲都有真实数据落地。 在 MT5 里把这段抄进 EA 或指标工程,改 m_handle 指向你用的振荡器,就能实测首次加载与后续刷新的拷贝行数差异——外汇与贵金属波动大,这类拷贝边界处理不当可能在高波动时漏柱,属高风险验证项。

MQL5 / C++
class COscUni_Calculate1: class="kw">public COscUni{
   class="kw">public:
      class="type">void COscUni_Calculate1(){
         m_bufferscnt=class="num">1;
      }
      class="kw">virtual class="type">int Calculate(class="kw">const class="type">int rates_total,
                     class="kw">const class="type">int prev_calculated,
                     class="type">class="kw">double &buffer0[],
                     class="type">class="kw">double &buffer1[]
      ){
         class="type">int cnt,start;
         if(prev_calculated==class="num">0){
            cnt=rates_total;
            start=class="num">0;
         }
         else{
            cnt=rates_total-prev_calculated+class="num">1;
            start=prev_calculated-class="num">1;
         }
         if(CopyBuffer(m_handle,class="num">0,class="num">0,cnt,buffer0)<=class="num">0){
             class="kw">return(class="num">0);
         }
         for(class="type">int i=start;i<rates_total;i++){
            buffer1[i]=EMPTY_VALUE;
         }
         class="kw">return(rates_total);
      }
};

class COscUni_Calculate2: class="kw">public COscUni{
   class="kw">public:
      class="type">void COscUni_Calculate2(){
         m_bufferscnt=class="num">2;
      }
      class="kw">virtual class="type">int Calculate(class="kw">const class="type">int rates_total,
                     class="kw">const class="type">int prev_calculated,
                     class="type">class="kw">double &buffer0[],
                     class="type">class="kw">double &buffer1[]
      ){
         class="type">int cnt;
         if(prev_calculated==class="num">0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+class="num">1;
         }
         if(CopyBuffer(m_handle,class="num">0,class="num">0,cnt,buffer0)<=class="num">0){
             class="kw">return(class="num">0);
         }
         if(CopyBuffer(m_handle,class="num">1,class="num">0,cnt,buffer1)<=class="num">0){
             class="kw">return(class="num">0);
         }
         class="kw">return(rates_total);
      }
};

「给震荡指标写子类时的参数接管逻辑」

通用震荡框架里,每个具体指标都该是一个独立子类,统一继承自 COscUni_Calculate1 或 COscUni_Calculate2。子类只留一个构造函数,把指标自身参数外加两个控制开关 use_default、keep_previous 一并收进来。use_default 决定要不要填默认值,keep_previous 则控制是全覆盖还是只补那些还没被碰过的参数。 ATR 子类是最省事的例子。ma_period 用引用传参,是为了在通用层设默认值时能反向写回。构造函数里这段判断是核心:当 use_default 且 keep_previous 为真,仅当 ma_period 还等于 -1 才赋 14;否则直接拍成 14。也就是说,通用层初始化前必须把所有参数先置 -1,才能靠这个机制识别“没用过”。 m_handle = iATR(Symbol(), Period(), ma_period); 这一行才真正把指标句柄建出来。后面几行纯属显示配置:m_name 拼成 "ATR(14)" 这类标签,m_drawtype1 设 DRAW_LINE,m_digits 取 _Digits+1 让小数位多一位,m_levels_total 给 0 表示不带水平线。 MACD 套路一样但字段更密。fast_ema_period 默认 12、slow_ema_period 默认 26、signal_period 默认 9、applied_price 默认 PRICE_CLOSE,keep_previous 下同样靠 -1 判断补缺。注意 applied_price 在子类原型里是 long&,标准枚举 ENUM_APPLIED_PRICE 本就是长整,用 -1 当“未设”哨兵不会类型冲突。iMACD 句柄建好后,m_drawtype1 配 DRAW_HISTOGRAM 画柱、m_drawtype2 配 DRAW_LINE 画信号线。 RSI 子类多一小段水平线配置:m_levels_total=3,数组扩到 3 位,分别填 30、50、70。这三档是裸眼读超买超卖的常用参照,外汇与贵金属波动剧烈,RSI 触 70/30 只代表概率倾斜,不代表反转必至,实操仍属高风险。其余震荡指标子类写法同构,完整集已在配套 CUniOsc.mqh 里,开 MT5 拖进去就能编译验证。

MQL5 / C++
class COscUni_ATR: class="kw">public COscUni_Calculate1{
   class="kw">public:
   class="type">void COscUni_ATR(class="type">bool use_default, class="type">bool keep_previous, class="type">int &ma_period){
   }
};

if(use_default){
   if(keep_previous){
      if(ma_period==-class="num">1)ma_period=class="num">14;
   }
   else{
      ma_period=class="num">14;
   }
}

m_handle=iATR(Symbol(),Period(),ma_period);
m_name=StringFormat("ATR(%i)",ma_period);
m_label1="ATR";
m_drawtype1=DRAW_LINE;
m_help=StringFormat("ma_period - Period1(%i)",ma_period);
m_digits=_Digits+class="num">1;
m_levels_total=class="num">0;

if(use_default){
   if(keep_previous){
      if(fast_ema_period==-class="num">1)fast_ema_period=class="num">12;
      if(slow_ema_period==-class="num">1)slow_ema_period=class="num">26;
      if(signal_period==-class="num">1)signal_period=class="num">9;
      if(applied_price==-class="num">1)applied_price=PRICE_CLOSE;
   }
   else{
      fast_ema_period=class="num">12;
      slow_ema_period=class="num">26;
      signal_period=class="num">9;
      applied_price=PRICE_CLOSE;
   }
}

m_handle=iMACD(Symbol(),
   Period(),
   fast_ema_period,
   slow_ema_period,
   signal_period,
   (ENUM_APPLIED_PRICE)applied_price);
m_name=StringFormat("iMACD(%i,%i,%i,%s)",
   fast_ema_period,
   slow_ema_period,
   signal_period,
   EnumToString((ENUM_APPLIED_PRICE)applied_price));
   m_label1="Main";
   m_label2="Signal";
   m_drawtype1=DRAW_HISTOGRAM;
   m_drawtype2=DRAW_LINE;
m_help=StringFormat("fast_ema_period - Period1(%i), "+
   "slow_ema_period - Period2(%i), "+
   "signal_period - Period3(%i), "+
   "applied_price - Price(%s)",
   fast_ema_period,
   slow_ema_period,
   signal_period,
   EnumToString((ENUM_APPLIED_PRICE)applied_price));
   m_digits=_Digits+class="num">1;

class="type">void COscUni_MACD(class="type">bool use_default,
   class="type">bool keep_previous,
   class="type">int &fast_ema_period,
   class="type">int &slow_ema_period,
   class="type">int &signal_period,
   class="type">long &applied_price){

m_levels_total=class="num">3;
ArrayResize(m_level_value,class="num">3);
m_level_value[class="num">0]=class="num">30;
m_level_value[class="num">1]=class="num">50;
m_level_value[class="num">2]=class="num">70;

◍ 搭一个可换类型的通用震荡指标骨架

震荡指标的类已经就绪,下一步是在 MT5 里新建一个名为 iUniOsc 的指标,向导里选 OnCalculate(...open,high,low,close) 这种带 OHLC 的重载,再开一个外部变量和两个线缓冲区。先把两个头文件塞到外部变量前:UniOscDefines.mqh 管枚举,CUniOsc.mqh 管通用类,不然后面编译器不认类型。 外部参数里先放一个 EOscUnyType 型变量 Type,默认 OscUni_ATR,再用 UseDefault 和 KeepPrev 两个 bool 控制是否走类里的默认值与保留前值逻辑。周期类参数统一给 Period1/2/3 = 14,MaMethod 默认 MODE_EMA,Price 默认 PRICE_CLOSE,Volume 默认 VOLUME_TICK,StPrice 默认 STO_LOWHIGH——这些就是后面 GUI 没接管前你能直接调的旋钮。 线颜色分三套:ColorLine1 亮绿画主线、ColorLine2 红画副线、ColorHisto 灰画柱。因为后面要做不重启就能换类型的 GUI,所以得给 Type 和那堆参数各留一份带下划线的副本变量(_Period1 等),以及 COscUni *osc 这个对象指针和 ProgName / ShortName 两个名字串。 PrepareParameters() 里有个细节:UseDefault 且 KeepPrev 同时为真时,所有 _Period 和 _MaMethod 等全赋 -1,类的构造函数看到 -1 就知道跳过去用内置默认,否则才用属性窗口的值。LoadOscillator() 按 _Type 走 switch 去 new 对应的子类(ATR、BearsPower、BullsPower…),new 完必须 osc.CheckHandle() 判错,失败就 Alert 并返回 INIT_FAILED。 SetStyles() 根据 BuffersCount() 是 1 还是 2 决定画一条还是两条:缓冲区 0 若是 DRAW_HISTOGRAM 就染成灰色,否则亮绿;缓冲区 1 存在才显示并染红。末尾用 IndicatorSetInteger(INDICATOR_DIGITS, osc.Digits()) 对齐小数位,再用 osc.LevelsTotal() 把水平线一次性灌进去。 OnInit() 里依次调 PrepareParameters → LoadOscillator → CheckHandle → SetStyles,最后 Print(osc.Help()) 把当前参数映射打到日志,并拼好 ShortName 设给 INDICATOR_SHORTNAME。到这步骨架就能跑,附件里的 iUniOsc 就是这版,后面改 GUI 时它会和现在略有出入。

MQL5 / C++
class="macro">#include <UniOsc/UniOscDefines.mqh>
class="macro">#include <UniOsc/CUniOsc.mqh>
input EOscUnyType          Type        =  OscUni_ATR;
input class="type">bool                 UseDefault  =  true;
input class="type">bool                 KeepPrev    =  true;
input class="type">int                  Period1     =  class="num">14;
input class="type">int                  Period2     =  class="num">14;
input class="type">int                  Period3     =  class="num">14;
input ENUM_MA_METHOD       MaMethod    =  MODE_EMA;
input ENUM_APPLIED_PRICE   Price       =  PRICE_CLOSE;
input ENUM_APPLIED_VOLUME  Volume      =  VOLUME_TICK;
input ENUM_STO_PRICE       StPrice     =  STO_LOWHIGH;
input class="type">class="kw">color                ColorLine1  =  clrLightSeaGreen;
input class="type">class="kw">color                ColorLine2  =  clrRed;
input class="type">class="kw">color                ColorHisto  =  clrGray;
class="type">int                  _Period1;
class="type">int                  _Period2;
class="type">int                  _Period3;
class="type">long                 _MaMethod;
class="type">long                 _Price;
class="type">long                 _Volume;
class="type">long                 _StPrice;
EOscUnyType          _Type;
COscUni * osc;
class="type">class="kw">string ProgName;
class="type">class="kw">string ShortName;
class="type">void PrepareParameters(){
   _Type=Type;
   if (UseDefault && KeepPrev){
      _Period1=-class="num">1;
      _Period2=-class="num">1;
      _Period3=-class="num">1;
      _MaMethod=-class="num">1;
      _Volume=-class="num">1;
      _Price=-class="num">1;
      _StPrice=-class="num">1;
   }
   else {
      _Period1=Period1;
      _Period2=Period2;
      _Period3=Period3;
      _MaMethod=MaMethod;
      _Volume= Volume;
      _Price=Price;
      _StPrice=StPrice;
   }
}
class="type">void LoadOscillator(){
   class="kw">switch (_Type){
      case OscUni_ATR:
         osc= new COscUni_ATR(UseDefault,KeepPrev,_Period1);
      class="kw">break;
      case OscUni_BearsPower:
         osc= new COscUni_BearsPower(UseDefault,KeepPrev,_Period1);
      class="kw">break;
      case OscUni_BullsPower:
         osc= new COscUni_BullsPower(UseDefault,KeepPrev,_Period1);
      class="kw">break;
   }
}
class="type">void SetStyles(){
   if (osc.BuffersCount()==class="num">2){
      PlotIndexSetInteger(class="num">0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(class="num">1,PLOT_DRAW_TYPE,osc.DrawType2());
      PlotIndexSetInteger(class="num">0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(class="num">1,PLOT_SHOW_DATA,true);
      PlotIndexSetString(class="num">0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(class="num">1,PLOT_LABEL,osc.Label2());
      if (osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,ColorHisto);
      }
      else {
         PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,ColorLine1);
      }
      PlotIndexSetInteger(class="num">1,PLOT_LINE_COLOR,ColorLine2);
   }
   else {
      PlotIndexSetInteger(class="num">0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(class="num">1,PLOT_DRAW_TYPE,DRAW_NONE);
      PlotIndexSetInteger(class="num">0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(class="num">1,PLOT_SHOW_DATA,class="kw">false);
      PlotIndexSetString(class="num">0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(class="num">1,PLOT_LABEL,"");
      if (osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,ColorHisto);
      }
      else {
         PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,ColorLine1);
      }
   }
   IndicatorSetInteger(INDICATOR_DIGITS,osc.Digits());
   class="type">int levels=osc.LevelsTotal();
   IndicatorSetInteger(INDICATOR_LEVELS,levels);
   for (class="type">int i=class="num">0;i<levels;i++){
      IndicatorSetDouble(INDICATOR_LEVELVALUE,i,osc.LevelValue(i));
   }
}
class="type">int OnInit(){
   SetIndexBuffer(class="num">0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,Label2Buffer,INDICATOR_DATA);
   PrepareParameters();
   LoadOscillator();
   if (!osc.CheckHandle()){
      Alert("载入指标时出错 "+osc.Name());
      class="kw">return(INIT_FAILED);
   }
   SetStyles();
   Print("参数对应: "+osc.Help());
   ShortName=ProgName+": "+osc.Name();
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
   class="kw">return(INIT_SUCCEEDED);
}

选 incGUI 搭面板前的落地动作

在 MT5 里做图形界面,路子不止一条:用原生图形对象手搓输入框和按钮是一种,也能直接搬社区里各种 MQL5 界面库,或者用终端自带的标准面板类。写这套系统时我把几类方案都过了一遍,最终落到 incGUI 开发库上——它用图形对象封装出表单、带调节钮的输入框和下拉列表,省掉大量底层绘制代码。 这个库出自「自定义图形化控件」系列第三篇的附件,核心是 incGUI_v3.mqh。MetaTrader 5 仍在频繁迭代,库里个别控件(比如滚动条)以后可能和终端新机制脱节,但当前版本照用不误。 落地只需一步:下载那篇第三部分的附件,解压后把 incGUI_v3.mqh 丢进终端数据目录的 MQL5/Include 下。重开 MT5 编辑器就能 #include <incGUI_v3.mqh> 起手,外汇与贵金属品种波动剧烈、杠杆风险高,界面只是辅助,信号逻辑仍须自行回测验证。

「把振荡器面板做成可交互表单」

图形界面单独放在 UniOscGUI.mqh 里写,先引 IncGUI_v4.mqh 和 UniOscDefines.mqh,再把 iUniOsc 复制成 iUniOscGUI,隐藏掉无意义的 UseDefault 和 KeepPrev 并强制置 false。iUniOscGUI 共包含三个头文件:UniOscDefines、CUniOsc、UniOscGUI,编译后图表上直接出 GUI。 表单类从 CFormTemplate 抄进 UniOscGUI.mqh 并重命名为 CUniOscForm,MainProperties() 里把宽度锁死为 FORM_WIDTH(210)、高度 150、可移动可缩放带关闭键。文件开头要定义三个常数:FORM_WIDTH=210、SPIN_BOX_WIDTH=110、COMBO_BOX_WIDTH=110,后期调控件尺寸就改这几个值。 指标里声明 input bool UseGUI=true,OnInit() 中 new 出表单并 Init、设子窗口、摆到 (10,30) 再 Show;OnDeinit() 里 Hide 加 delete。OnChartEvent() 把事件透传给 frm.Event()。点关闭按钮时 Event() 返回 1,用 ChartWindowFind(0,ShortName) 拿子窗口索引,再 ChartIndicatorDelete 把指标从图表删掉,外汇/贵金属图表加载自定义指标本就高风险,误删重加成本不高但打断盯盘节奏。 下拉列表用 CComBox 做,m_cmb_main.Init 传前缀名、宽度 100、标签“选择震荡指标”,OnShowEvent 里偏移 (10,10) 显示。选择变化事件不从表单内部处理,而在指标 OnChartEvent 调 m_cmb_main.Event,返回 1 就 Alert 当前 SelectedText。 填充列表走最省事的第 3 种:指标里建 EOscUniType 数组,涵盖 ATR 到 WPR 共 15 个振荡器,frm.Init 之后循环 AddItem(EnumToString) 并 SetSelectedIndex(0)。此时图表表单已能下拉且切换弹框,MT5 里加载验证一遍即可确认。

MQL5 / C++
class="macro">#include <IncGUI_v4.mqh>
class="macro">#include <UniOsc/UniOscDefines.mqh>
class="macro">#include <UniOsc/CUniOsc.mqh>
class="macro">#include <UniOsc/UniOscGUI.mqh>

class="type">bool UseDefault = class="kw">false;
class="type">bool KeepPrev = class="kw">false;

input class="type">bool UseGUI = true;
CUniOscForm * frm;

class="type">void MainProperties(){
   m_Name = "UniOscForm";
   m_Width = FORM_WIDTH;
   m_Height = class="num">150;
   m_Type = class="num">0;
   m_Caption = "UniOsc";
   m_Movable = true;
   m_Resizable = true;
   m_CloseButton = true;
}

class="macro">#define FORM_WIDTH class="num">210
class="macro">#define SPIN_BOX_WIDTH class="num">110
class="macro">#define COMBO_BOX_WIDTH class="num">110

class="type">int OnInit(){
   if(UseGUI){
      frm = new CUniOscForm();
      frm.Init();
      frm.SetSubWindow(class="num">0);
      frm.SetPos(class="num">10,class="num">30);
      frm.Show();
   }
   class="kw">return(INIT_SUCCEEDED);
}

class="type">void OnDeinit(class="kw">const class="type">int reason){
   if(CheckPointer(frm)==POINTER_DYNAMIC){
      frm.Hide();
      class="kw">delete(frm);
   }
}

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){
   frm.Event(id,lparam,dparam,sparam);
   class="type">int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
   if(me==class="num">1){ Alert(frm.m_cmb_main.SelectedText()); }
}

EOscUniType osctype[]={
   OscUni_ATR, OscUni_BearsPower, OscUni_BullsPower, OscUni_CCI,
   OscUni_Chaikin, OscUni_DeMarker, OscUni_Force, OscUni_Momentum,
   OscUni_MACD, OscUni_OsMA, OscUni_RSI, OscUni_RVI,
   OscUni_Stochastic, OscUni_TriX, OscUni_WPR
};

for(class="type">int i=class="num">0;i<ArraySize(osctype);i++){ frm.m_cmb_main.AddItem(EnumToString(osctype[i])); }
frm.m_cmb_main.SetSelectedIndex(class="num">0);

class="type">int win=ChartWindowFind(class="num">0,ShortName);
ChartIndicatorDelete(class="num">0,win,ShortName);
ChartRedraw();
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多震荡指标同屏叠加的预设,把重复劳动交给小布,你专注决策。

常见问题

基类统一接口、子类传构造参数,MetaEditor 会提示参数列表,开发时不容易填错,也方便后续用 iCustom 统一调用。
小布盯盘的品种页已内置多指标同屏与参数快调,逻辑与本篇基类+表单思路一致,无需自己编译即可看盘口变化。
iCustom 限 64 个、IndicatorCreate 限 256 个参数且含通用项,实际更少;GUI 把具体指标参数收进对话框,外部只留少量通用参数。
表单类管界面布局与控件响应,选定某指标后动态显示其专属参数控件,避免把所有震荡参数都摊在外部窗口。