利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加
🗂️

利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加"homework"选项卡并保存图形对象·进阶篇

(2/3)· 还在纸上记品种代码?本篇用 MQL5/MQL4 条件编译与 homework 选项卡自动保存图表对象

偏理论进阶 第 2/3 篇
把品种代码抄在纸上不仅毁森林,更会在切换周期时丢失你的分析上下文。很多交易者重复画支撑阻力线,却从没想过让实用程序替你记住这些图形对象。

◍ 面板按钮的状态轮询与品种增删实现

这段逻辑跑在图表对象的事件循环里,核心是用 ObjectGetInteger 读按钮的 OBJPROP_STATE,判断用户点了「Add」还是「Del」。一旦某个 panelto 按钮被置真,就调用 addToPanel 把当前图表的品种塞进对应选卡,再 nextchart 跳到下一个品种;若是 panelfrom 按钮置真,则 delToPanel 移除后 curchart 刷新当前图。 按钮初始化时每行占 25 像素(tmpHeight+=25),panelNames 数组从索引 1 开始遍历,说明索引 0 大概率预留给「全部」或主选卡。OBJPROP_SELECTABLE 设为 false 意味着这些按钮不参与手动框选,只响应 State 点击。 删除函数 delToPanel 先线性扫 arrPanels[num] 找到同名品种再 Delete,若删的是当前选卡(num==cur_panel)就 ObjectsDeleteAll(0, exprefix) 清掉旧按钮对象,重新 show_panel_buttons 和 show_symbols;非当前选卡只 upd_panel_title 轻量更新。添加函数 addToPanel 在目标选卡 Add 后,会反向遍历其余选卡把同名品种剔掉,保证一个品种只存在于一个分组。 开 MT5 把这段嵌进你的面板 EA,故意让同一个品种在两组间反复点 Add,能直接验证「自动去重」是否按预期触发。外汇与贵金属品种切换频繁,这类分组误操作可能引发错单,实盘前务必在模拟环境跑通。

MQL5 / C++
ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_SELECTABLE,false);
ObjectSetString(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_TEXT,"Add "+panelNames[i]);
ObjectSetInteger(CID,exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_BGCOLOR,clrHoneydew);
}
tmpHeight+=class="num">25;
   for( class="type">uchar i=class="num">1; i<ArraySize(panelNames); i++ ){
      class=class="str">"cmt">// 如果从选卡中删除的按钮被按下,首先,删除品种,并再次打开品种图表
      if(ObjectGetInteger(curChartID[tmpCIDcnt-class="num">1],exprefix+"_p_btn_panelfrom"+(class="type">class="kw">string) i,OBJPROP_STATE)==true ){
         delToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-class="num">1]));
         curchart();
         class="kw">return;
      }
      class=class="str">"cmt">// 如果添加到选项卡的按钮被按下,则首先添加品种,然后打开下一个品种图表
      if(ObjectGetInteger(curChartID[tmpCIDcnt-class="num">1],exprefix+"_p_btn_panelto"+(class="type">class="kw">string) i,OBJPROP_STATE)==true ){
         addToPanel(i, ChartSymbol(curChartID[tmpCIDcnt-class="num">1]));
         nextchart();
         class="kw">return;
      }
   }
class="type">void delToPanel(class="type">uchar num, class="type">class="kw">string name){
  class=class="str">"cmt">// 沿整个数组移动并删除第一个元素
  class=class="str">"cmt">// 其名称与我们的品种相似
  for(class="type">int i=class="num">0; i<arrPanels[num].Total(); i++){
    if( arrPanels[num].At(i)==name ){
      arrPanels[num].Delete(i);
      break;
    }
  }
  class=class="str">"cmt">// 如果选卡当前已打开,
  if(num==cur_panel){
    initial_btn_line();
    class=class="str">"cmt">// 从图表中删除以前创建的品种按钮:
    ObjectsDeleteAll(class="num">0, exprefix);
    class=class="str">"cmt">// 显示更新后的品种列表:
    show_panel_buttons();
    show_symbols();
  }else{
    class=class="str">"cmt">// 如果打开任何其他选项卡,只需更新标题按钮
    upd_panel_title();
  }


}
class="type">void addToPanel(class="type">uchar num, class="type">class="kw">string name){
  class=class="str">"cmt">// 在选卡中添加一个品种
  arrPanels[num].Add(name);
  class=class="str">"cmt">// 从其他选卡中删除品种
  class=class="str">"cmt">// 如果存在的话
  for( class="type">int j=class="num">1; j<ArraySize(arrPanels); j++ ){
    if(j==num) class="kw">continue;
    for(class="type">int i=class="num">0; i<arrPanels[j].Total(); i++){
      if( arrPanels[j].At(i)==name ){

「面板状态落盘与重载的实现细节」

在 MT5 面板类 EA 里,用户切换品种或删掉某个子面板后,如果不把状态写回文件,下次加载图表就会丢失布局。上面这段逻辑把 arrPanels 数组从下标 1 开始遍历,依次以 panelN.bin 的二进制 ANSI 格式落盘,N 对应 i+1,因此实际文件名为 panel2.bin、panel3.bin……

保存函数 savePanels() 用 FileOpen 带 FILE_WRITEFILE_BINFILE_ANSI 打开句柄,fh>=0 才调用 arrPanels[i].Save(fh) 并关闭;读回时换成 FILE_READ 标志,Load 进同样结构。注意数组 0 号位被刻意跳过,说明 0 槽可能留作主控或占位。

前面那段删除分支也值得盯:当 panelval==i 且 i>0 时先自减再 Delete(i) 并 break,保证面板序号在删中间项后不出现空洞。若 num==cur_panel 还会重画按钮和标题,否则只 upd_panel_title() 轻量刷新。 开 MT5 把这段接进自己的面板类,改 exprefix 前缀后点几次切换品种,到终端 Data 文件夹翻 panel2.bin 大小变化,就能验证写入是否随操作实时发生。外汇与贵金属品种多、切换频,这类状态持久化在高波动时段误关图表可能丢掉自选组合,属高风险操作环境。

MQL5 / C++
if( panelval==i && i>class="num">0 ){
      panelval--;
     }
     arrPanels[j].Delete(i);
     break;
    }
   }
  }
  if(num==cur_panel){
   initial_btn_line();
   class=class="str">"cmt">// 从图表中删除以前创建的品种按钮:
   ObjectsDeleteAll(class="num">0, exprefix);
   class=class="str">"cmt">// 显示品种列表:
   show_panel_buttons();
   show_symbols();
  }else{
   upd_panel_title();
  }
}
class="type">void savePanels(){
  for( class="type">int i=class="num">1; i<ArraySize(arrPanels); i++ ){
    fh=FileOpen(exprefix+"panel"+(class="type">class="kw">string) (i+class="num">1)+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI);
    if(fh>=class="num">0){
      arrPanels[i].Save(fh);
      FileClose(fh);
    }
  }
}
  for( class="type">int i=class="num">1; i<ArraySize(arrPanels); i++ ){
    fh=FileOpen(exprefix+"panel"+(class="type">class="kw">string) (i+class="num">1)+".bin",FILE_READ|FILE_BIN|FILE_ANSI);
    if(fh>=class="num">0){
      arrPanels[i].Load(fh);
      FileClose(fh);
    }
  }

给参数集加个可见的身份标签

跨市场切换时,最烦的不是调参数,而是忘了当前加载的是哪一套。做美股开盘半小时内突破,和做俄股、欧股完全是两套品种池;外汇和股票也该分开管。手动记容易乱,几秒加载的设置文件若没有标识,盯盘时根本分不清此刻跑的是哪国逻辑。 加一个 cmt 输入字符串就能解决。它既是面板上的注释,也能嵌进 homework 选卡的文件名,把不同市场的自选品种列表隔离保存。下面这段代码在显示按钮后执行:若 cmt 非空,就在按钮行用 OBJ_EDIT 画一条金色注释,宽 133 像素、字号 8,位置由 btn_left+11 和 BTN_HEIGHT*btn_line 定。 保存函数也要改——savePanels 里把 cmt 的空格替换成下划线,拼进文件名 panel{i+1}{tmpCmt}.bin。这样美股设置存成 panel2US_Stock.bin,俄股存成 panel2RU_.bin,还原时一一对应,不会把 A 市场的 homework 列表塞进 B 市场。 别把注释当摆设 cmt 空着时面板不画标题,但文件保存仍会生成带空标识的 bin。多人共用 MT5 或经常换品种,建议强制填 cmt,否则三个月后你翻文件会怀疑人生。外汇贵金属波动大、跳空频繁,参数集隔离后仍需人工复核品种流动性。

MQL5 / C++
class="kw">input class="type">class="kw">string        cmt=""; class=class="str">"cmt">// 用于 (eng) 的参数
  class=class="str">"cmt">// 如果指定,则显示注释:
  if(StringLen(cmt)>class="num">0){
    class="type">class="kw">string tmpCMT=cmt;
    ObjectCreate(class="num">0, exprefix+"title", OBJ_EDIT, class="num">0, class="num">0, class="num">0);
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_XDISTANCE,btn_left+class="num">11); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_XSIZE,class="num">133); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_YSIZE,BTN_HEIGHT); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_FONTSIZE,class="num">8); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_COLOR,clrGold); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_BGCOLOR,clrNONE); 
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_BORDER_COLOR,clrBlack);
    ObjectSetString(class="num">0,exprefix+"title",OBJPROP_TEXT,tmpCMT);
    ObjectSetInteger(class="num">0,exprefix+"title",OBJPROP_SELECTABLE,false);
  }
class="type">void savePanels(){
  class="type">class="kw">string tmpCmt=cmt;
  StringReplace(tmpCmt, " ", "_");
  for( class="type">int i=class="num">1; i<ArraySize(arrPanels); i++ ){
    fh=FileOpen(exprefix+"panel"+(class="type">class="kw">string) (i+class="num">1)+tmpCmt+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI); 
    if(fh>=class="num">0){ 
      arrPanels[i].Save(fh);
      FileClose(fh);
    }
  }
}

◍ 图表重开别让画线全丢

做价格行为分析的人,最烦的就是关掉 MT5 图表再打开,几十根手画的水平线、文本标签全没了。理想状态是:关图前把图形对象存盘,重开时自动还原,不用手动重画。 MQL4 和 MQL5 在存图形对象这事上底层差异很大。MQL4 用数字常量描述对象类型和属性,一个字符串就能拼出全部信息;MQL5 改用 enum 枚举,每种对象、每个属性都得单独写字符串,通用保存函数基本只够应付水平线、标签和文本三种。如果你要在 MT5 上存趋势线、矩形之类的,得自己补逻辑。 下面这段是实际可用的保存函数核心。它只在 saveGraphics 开关为真时执行,遍历图表上所有对象,过滤掉 EA 自己生成的挂单/止损类前缀,把用户对象名和关键属性拼成管道符分隔的字符串存进数组。 调用时机很关键:loadchart 要塞进 showcharts(按按钮开图时),savechart 则挂在 Next chart / Prev chart / Close chart 以及 homework 选卡增删品种的按钮响应里。外汇和贵金属波动快、跳空多,重开图表若丢了关键价位线,可能直接影响你对结构的判断,属于高风险操作环节,建议先在模拟账户验证。

MQL5 / C++
  class="type">void savechart(class="type">class="kw">ulong id){
      class=class="str">"cmt">// 保存图形对象,仅在 
      class=class="str">"cmt">// "Save created graphical objects" 参数 = true
      if(saveGraphics){
          class=class="str">"cmt">// 获取品种名称
          class="type">class="kw">string tmpName="";
          if(cur_panel<ArraySize(arrPanels)){
              tmpName=arrPanels[cur_panel][panelval];
          }
          tmpName=clean_symbol_name(tmpName);
          StringReplace(tmpName, " ", "");
          
          class=class="str">"cmt">// 清除图形对象数组
          saveG.Resize(class="num">0);
          
          class=class="str">"cmt">// 将所有用户创建的图形对象及其属性添加到数组中
          class="type">int obj_total=ObjectsTotal((class="type">long) id);
          class="type">class="kw">string name;
          class="type">class="kw">string tmpObjLine="";
          for(class="type">int i=class="num">0;i<obj_total;i++){
              name = ObjectName((class="type">long) id, i);
              if( StringFind(name, exprefix)<class="num">0 && StringFind(name, "fix")<class="num">0 && StringFind(name, "take")<class="num">0 && StringFind(name, "stop loss")<class="num">0 && StringFind(name, "sell")<class="num">0 && StringFind(name, "buy")<class="num">0 ){
                   tmpObjLine=name;
                  
                   StringAdd(tmpObjLine, "|class="type">int~OBJPROP_TYPE~"+ (class="type">class="kw">string)(class="type">int) OBJPROP_TYPE +"~"+ (class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_TYPE));
                   StringAdd(tmpObjLine, "|class="type">int~OBJPROP_COLOR~"+ (class="type">class="kw">string)(class="type">int) OBJPROP_COLOR +"~"+ (class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_COLOR));
                   StringAdd(tmpObjLine, "|class="type">int~OBJPROP_STYLE~"+ (class="type">class="kw">string)(class="type">int) OBJPROP_STYLE +"~"+ (class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_STYLE));

「把图形对象的整型属性一次性dump出来」

在 MT5 里批量导出图表对象时,很多整型属性容易被漏掉。下面这段拼接逻辑把 14 个常用 OBJPROP_* 整型字段依次塞进同一行字符串,用「|int~属性枚举~属性值」的格式隔离,方便后续解析或写日志。 注意 OBJPROP_WIDTH 是线宽(1~5 像素),OBJPROP_TIME 返回的是锚点时间(datetime 整数),OBJPROP_TIMEFRAMS 用位掩码表示可见周期,OBJPROP_STATE 对按钮类对象才有意义(0/1)。这些字段在复制对象模板时若不一致,可能导致新对象显示错位。 外汇与贵金属市场杠杆高、波动剧烈,用脚本批量读写对象属性时务必先在模拟盘验证,避免误改实盘图表。直接把下面代码贴进 EA 的循环里,把 id 和 name 换成你的图表句柄与对象名即可跑通。

MQL5 / C++
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_WIDTH~"+(class="type">class="kw">string)(class="type">int) OBJPROP_WIDTH+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_WIDTH));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_TIME~"+(class="type">class="kw">string)(class="type">int) OBJPROP_TIME+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_TIME));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_TIMEFRAMES~"+(class="type">class="kw">string)(class="type">int) OBJPROP_TIMEFRAMES+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_TIMEFRAMES));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_ANCHOR~"+(class="type">class="kw">string)(class="type">int) OBJPROP_ANCHOR+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_ANCHOR));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_XDISTANCE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_XDISTANCE+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_XDISTANCE));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_YDISTANCE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_YDISTANCE+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_YDISTANCE));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_STATE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_STATE+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_STATE));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_XSIZE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_XSIZE+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_XSIZE));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_YSIZE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_YSIZE+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_YSIZE));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_XOFFSET~"+(class="type">class="kw">string)(class="type">int) OBJPROP_XOFFSET+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_XOFFSET));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_YOFFSET~"+(class="type">class="kw">string)(class="type">int) OBJPROP_YOFFSET+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_YOFFSET));
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_BGCOLOR~"+(class="type">class="kw">string)(class="type">int) OBJPROP_BGCOLOR+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_BGCOLOR));

把图形对象落盘再回灌图表

导出逻辑里用 StringAdd 把边框色、价格、文本三类属性拼进同一行:边框色走 ObjectGetInteger,价格走 ObjectGetDouble,文本走 ObjectGetString,属性枚举先强转 int 再转 string 做字段标识。拼完由 saveG.Add 收进数组,最后 FileOpen 以 FILE_WRITEFILE_BINFILE_ANSI 开 .bin 写入并关句柄。

回读时 loadchart 先按面板取品种名,clean_symbol_name 后再剔空格,避免文件名带空格导致 FileOpen 失败。saveG.Resize(0) 清旧数组,FILE_READ 模式把 .bin 灌回 saveG,循环里用 '|' 和 '~' 两级分隔做 StringSplit,逐字段还原对象。 这套机制只在 saveGraphics 开关为真时触发,实盘加载前建议先在 EURUSD 的 M1 上手动画三个对象验证 .bin 生成大小约为 120~400 字节/对象,外汇与贵金属杠杆高,回灌对象若叠加在错误周期上可能干扰你的价格行为判断。

MQL5 / C++
StringAdd(tmpObjLine, "|class="type">int~OBJPROP_BORDER_COLOR~"+(class="type">class="kw">string)(class="type">int) OBJPROP_BORDER_COLOR+"~"+(class="type">class="kw">string) ObjectGetInteger(id, name, OBJPROP_BORDER_COLOR));
StringAdd(tmpObjLine, "|class="type">class="kw">double~OBJPROP_PRICE~"+(class="type">class="kw">string)(class="type">int) OBJPROP_PRICE+"~"+(class="type">class="kw">string) ObjectGetDouble(id, name, OBJPROP_PRICE));
StringAdd(tmpObjLine, "|class="type">class="kw">string~OBJPROP_TEXT~"+(class="type">class="kw">string)(class="type">int) OBJPROP_TEXT+"~"+(class="type">class="kw">string) ObjectGetString(id, name, OBJPROP_TEXT));

saveG.Add(tmpObjLine);
}
}
class=class="str">"cmt">// 将数组内容保存到文件中
fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_WRITE|FILE_BIN|FILE_ANSI);
if(fh>=class="num">0){
saveG.Save(fh);
FileClose(fh);
}
}
}
class="type">void loadchart(class="type">class="kw">ulong id){
class=class="str">"cmt">// 显示图形对象,仅在
class=class="str">"cmt">// "Save created graphical objects" 输入 = true
if(saveGraphics){
class=class="str">"cmt">// 获取品种名称
class="type">class="kw">string tmpName="";
if(cur_panel<ArraySize(arrPanels)){
tmpName=arrPanels[cur_panel][panelval];
}
tmpName=clean_symbol_name(tmpName);
StringReplace(tmpName, " ", "");

class="type">class="kw">string tmpObjLine[];
class="type">class="kw">string tmpObjName="";
class="type">class="kw">string sep1="|";
class="type">class="kw">string sep2="~";

class=class="str">"cmt">// 清除图形对象数组
saveG.Resize(class="num">0);
class=class="str">"cmt">// 将图形对象列表从文件加载到数组
fh=FileOpen(exprefix+"_graph_"+tmpName+".bin",FILE_READ|FILE_BIN|FILE_ANSI);
if(fh>=class="num">0){
saveG.Load(fh);
FileClose(fh);
}
class=class="str">"cmt">// 在图表上连续显示图形对象
for( class="type">int i=class="num">0; i<saveG.Total(); i++ ){
StringSplit(saveG.At(i), StringGetCharacter(sep1,class="num">0), tmpObjLine);
for( class="type">int j=class="num">0; j<ArraySize(tmpObjLine); j++ ){
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到对象保存状态,把重复劳动交给小布,你专注决策。

常见问题

两者均支持根据当前语言版本执行不同代码块的结构,可在同一源码中维护双端逻辑,避免手动替换模块。
不能限定。默认有 Long、Short、Range 三个选卡,但内容由你决定,可放任意自选定品种或分析标记。
取决于终端存储机制与文件同步设置,概率上本地保存更稳,跨设备需确认配置文件随账户迁移。
可以。小布盯盘对应品种页会读取图表对象层,提示哪些线是上次会话保留的,减少重画时间。
它在面板标明当前过滤与选卡状态,让你在多个品种间跳转时快速回忆当初的筛选逻辑,倾向降低误判。