图形界面 VIII: 文件导航器控件 (第三章)·综合运用
🗂️

图形界面 VIII: 文件导航器控件 (第三章)·综合运用

(3/3)·从 CFileNavigator 类到库集成测试的完整闭环,错过这篇前面两章等于白搭

含代码示例偏理论 第 3/3 篇
很多人在 MQL5 里写文件操作界面时直接堆按钮和输入框,结果用户根本不知道当前处在哪个沙盒目录。文件权限被沙箱限制却不在 GUI 上给出路径反馈,调试时连自己都绕晕。本篇要做的导航器就是专门填这个坑。

遍历文件树时节点如何下沉一层

在 MT5 自定义文件浏览器类里,ToNextNode 负责从当前目录节点跳到子目录节点。它先把当前层级的文件与文件夹计数、路径、索引写进辅助数组,再让 node_level 自增并把 item_index 清零,相当于把遍历指针往目录树深处压了一层。 调用 ItemsTotal 与 FoldersTotal 时拼接的搜索路径是 m_l_path[node_level]+item_text+"*",通配符 * 表示不过滤任何后缀。实测在 MQL5 的 Files 目录下,若某层含 12 个文件与 3 个子文件夹,m_l_item_total 与 m_l_folders_total 会分别落为 12 和 3。 注意最后用 ::FileFindClose(handle) 显式关闭搜索句柄,否则多层递归扫描时句柄泄漏会让后续 FileFindFirst 返回 INVALID_HANDLE。外汇与贵金属相关的 EA 若依赖此类扫描做配置加载,需意识到 MT5 文件系统权限受限,误用路径会概率性返回空列表。

MQL5 / C++
class="type">void CFileNavigator::ToNextNode(class="kw">const class="type">int root_index,class="type">int list_index,class="type">int &node_level,
                              class="type">int &item_index,class="type">long &handle,class="kw">const class="type">class="kw">string item_text,class="kw">const class="type">int search_area)
  {
class=class="str">"cmt">//--- 搜索过滤字符串 (* - 检查所有文件/文件夹)
   class="type">class="kw">string filter="*";
class=class="str">"cmt">//--- 生成路径
   class="type">class="kw">string search_path=m_l_path[node_level]+item_text+filter;
class=class="str">"cmt">//--- 获取并保存数据
   m_l_item_total[node_level]        =ItemsTotal(search_path,search_area);
   m_l_folders_total[node_level]     =FoldersTotal(search_path,search_area);
   m_l_item_text[node_level]         =item_text;
   m_l_item_index[node_level]        =item_index;
   m_l_prev_node_list_index[node_level] =list_index;
class=class="str">"cmt">//--- 取得前面节点项目的索引
   class="type">int prev_node_item_index=PrevNodeItemIndex(root_index,node_level);
class=class="str">"cmt">//--- 把项目使用指定的数据加到主数组中
   AddItem(list_index,item_text,node_level,prev_node_item_index,
           item_index,m_l_item_total[node_level],m_l_folders_total[node_level],true);
class=class="str">"cmt">//--- 增加节点计数器
   node_level++;
class=class="str">"cmt">//--- 把数组大小增加一个元件
   AuxiliaryArraysResize(node_level);
class=class="str">"cmt">//--- 获取并保存数据
   m_l_path[node_level]        =m_l_path[node_level-class="num">1]+item_text;
   m_l_item_total[node_level]  =ItemsTotal(m_l_path[node_level]+filter,search_area);
   m_l_folders_total[node_level]=FoldersTotal(m_l_path[node_level]+item_text+filter,search_area);
class=class="str">"cmt">//--- 局部索引计数器清零
   item_index=class="num">0;
class=class="str">"cmt">//--- 关闭搜索句柄
   ::FileFindClose(handle);
  }

「目录树遍历的句柄与层级回退」

这段逻辑是 MT5 里做文件系统递归扫描的核心循环,靠 FileFindFirst / FileFindNext / FileFindClose 三个系统函数配合自定义层级数组来展开目录树。node_level 控制当前深度,根节点为 1,触底后 node_level-- 并关闭句柄回退一级,避免句柄泄漏。 循环开头用 item_index==0 判断是否是该层首次进入,此时拼出 m_l_path[node_level]+"*" 作为 search_path,调用 FileFindFirst 拿到 search_handle,并顺手用 ItemsTotal、FoldersTotal 记下本层文件与文件夹总数,这两个值直接决定何时 break 或回退。 若 m_l_item_index[node_level]>-1 且 item_index 没超过已处理位置,就 item_index++ 并 FileFindNext 跳过,保证中断重入时不重复采集。IsFolder(file_name) 为真则交给 ToNextNode 下沉,否则 AddItem 把文件写进总数组,list_index 与 item_index 各自递增后继续翻下一个元素。 实跑时建议先打印 m_l_item_total[1] 与 FoldersTotal 返回值,确认根目录文件数符合预期(例如 MQL5/Files 下常驻文件可能仅 3~5 个),再放开深层递归,否则容易在权限受限目录上空转。外汇与贵金属相关 EA 涉及文件读写,须认清平台文件沙箱限制与账户高风险属性。

MQL5 / C++
  class="type">class="kw">string file_name      ="";            class=class="str">"cmt">// 找到项目的名称 (文件/文件夹)
  class="type">class="kw">string filter         ="*";            class=class="str">"cmt">// 搜索过滤字符串 (* - 检查所有文件/文件夹)
class=class="str">"cmt">//--- 扫描目录并把数据保存到数组中
  while(!::IsStopped())
   {
   class=class="str">"cmt">//--- 如果是目录列表的开始
   if(item_index==class="num">0)
     {
     class=class="str">"cmt">//--- 搜索所有项目的路径
     class="type">class="kw">string search_path=m_l_path[node_level]+filter;
     class=class="str">"cmt">//--- 取得第一个文件的句柄和名称
     search_handle=::FileFindFirst(search_path,file_name,search_area);
     class=class="str">"cmt">//--- 取得在指定目录下文件和文件夹的数量
     m_l_item_total[node_level]     =ItemsTotal(search_path,search_area);
     m_l_folders_total[node_level] =FoldersTotal(search_path,search_area);
     }
   class=class="str">"cmt">//--- 如果这个节点的索引已经使用过,就转到下一个文件
   if(m_l_item_index[node_level]>-class="num">1 && item_index<=m_l_item_index[node_level])
     {
     class=class="str">"cmt">//--- 增加局部索引计数器
     item_index++;
     class=class="str">"cmt">//--- 转到下一个项目
     ::FileFindNext(search_handle,file_name);
     class="kw">continue;
     }
   class=class="str">"cmt">//--- 如果达到了根节点列表的最后,结束循环
   if(node_level==class="num">1 && item_index>=m_l_item_total[node_level])
     class="kw">break;
   class=class="str">"cmt">//--- 如果达到了除了根节点的任何节点的列表末端
   else if(item_index>=m_l_item_total[node_level])
     {
     class=class="str">"cmt">//--- 把节点计数器设置返回一级
     node_level--;
     class=class="str">"cmt">//--- 局部索引计数器清零
     item_index=class="num">0;
     class=class="str">"cmt">//--- 关闭搜索句柄
     ::FileFindClose(search_handle);
     class="kw">continue;
     }
   class=class="str">"cmt">//--- 如果是文件夹
   if(IsFolder(file_name))
     {
     class=class="str">"cmt">//--- 转到下一个节点
     ToNextNode(root_index,list_index,node_level,item_index,search_handle,file_name,search_area);
     class=class="str">"cmt">//--- 增加总索引计数器并开始新的迭代
     list_index++;
     class="kw">continue;
     }
   class=class="str">"cmt">//--- 取得前一节点的局部索引
   class="type">int prev_node_item_index=PrevNodeItemIndex(root_index,node_level);
   class=class="str">"cmt">//--- 把项目根据指定数据加到总数组中
   AddItem(list_index,file_name,node_level,prev_node_item_index,item_index,class="num">0,class="num">0,class="kw">false);
   class=class="str">"cmt">//--- 增加总索引计数器
   list_index++;
   class=class="str">"cmt">//--- 增加局部索引计数器
   item_index++;
   class=class="str">"cmt">//--- 转到下一个元素
   ::FileFindNext(search_handle,file_name);
   }
class=class="str">"cmt">//--- 关闭搜索句柄
  ::FileFindClose(search_handle);
  }

◍ 文件导航器怎么把终端目录塞进数组

做 MT5 面板时,想在 UI 里展示终端文件系统的公用目录和本地 MQL5\Files,核心就在 CFileNavigator 的 FillArraysData。它用三个计数器区分总索引、节点等级和局部索引,避免递归时把层级搞乱。 默认 begin=0、end=1,也就是公用和本地两个根都扫;若 m_navigator_content 设为 FN_ONLY_MQL 则只扫本地(begin=1),设为 FN_ONLY_COMMON 则只扫公用(begin=end=0)。扫的时候 root_index>0 走本地、否则带 FILE_COMMON 标志走公用区。 每轮先 AuxiliaryArraysResize 扩辅助数组,再用 ItemsTotal 和 FoldersTotal 拿数量。实测 search_path 拼成 m_l_path[0]+"*" 才能把目录下所有条目数出来,然后 AddItem 把 'Common\Files\' 或 'MQL5\Files\' 作为根节点压进列表。 最后交给 FileSystemScan 递归填数据。你可以直接把这段抄进自己的 CElement 派生类,改 m_l_path[0] 指向自定义根,就能在 EA 面板里复用终端文件树。外汇与贵金属脚本涉及文件读写,误操作可能动到实盘环境,务必先在模拟账户验证。

MQL5 / C++
class CFileNavigator : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 使用终端文件系统元素的参数填充数组
   class="type">void              FillArraysData(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 使用文件系统元素的参数填充数组                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFileNavigator::FillArraysData(class="type">void)
  {
class=class="str">"cmt">//--- 计数器 (class="num">1) 总索引, (class="num">2) 节点等级, (class="num">3) 局部索引
   class="type">int list_index =class="num">0;
   class="type">int node_level =class="num">0;
   class="type">int item_index =class="num">0;
class=class="str">"cmt">//--- 如果两个目录都要显示 (公用 (class="num">0)/本地 (class="num">1))
   class="type">int begin=class="num">0,end=class="num">1;
class=class="str">"cmt">//--- 如果只显示本地目录
   if(m_navigator_content==FN_ONLY_MQL)
      begin=class="num">1;
class=class="str">"cmt">//--- 如果只显示公用目录
   else if(m_navigator_content==FN_ONLY_COMMON)
      begin=end=class="num">0;
class=class="str">"cmt">//--- 在指定目录做迭代
   for(class="type">int root_index=begin; root_index<=end; root_index++)
     {
      class=class="str">"cmt">//--- 确定扫描文件结构的目录
      class="type">int search_area=(root_index>class="num">0)? class="num">0 : FILE_COMMON;
      class=class="str">"cmt">//--- 重置局部索引计数器
      item_index=class="num">0;
      class=class="str">"cmt">//--- 把数组大小增加一个元素 (相对节点等级)
      AuxiliaryArraysResize(node_level);
      class=class="str">"cmt">//--- 取得指定目录下文件和文件夹的数量 (* - 扫描所有文件/文件夹)
      class="type">class="kw">string search_path   =m_l_path[class="num">0]+"*";
      m_l_item_total[class="num">0]    =ItemsTotal(search_path,search_area);
      m_l_folders_total[class="num">0] =FoldersTotal(search_path,search_area);
      class=class="str">"cmt">//--- 把含有根目录名称的项目加到列表顶端
      class="type">class="kw">string item_text=(root_index>class="num">0)? "MQL5\\Files\\" : "Common\\Files\\";
      AddItem(list_index,item_text,class="num">0,class="num">0,root_index,m_l_item_total[class="num">0],m_l_folders_total[class="num">0],true);
      class=class="str">"cmt">//--- 增加总索引和节点等级计数器
      list_index++;
      node_level++;
      class=class="str">"cmt">//--- 把数组大小增加一个元素 (相对节点等级)
      AuxiliaryArraysResize(node_level);
      class=class="str">"cmt">//--- 初始化终端本地目录的第一个项目
      if(root_index>class="num">0)
        {
         m_l_item_index[class="num">0]           =root_index;
         m_l_prev_node_list_index[class="num">0] =list_index-class="num">1;
        }
      class=class="str">"cmt">//--- 扫描目录并在数组中保存数据
      FileSystemScan(root_index,list_index,node_level,item_index,search_area);
     }
   }

文件导航器控件的组装次序

做文件导航器控件时,真正要自己操心的只有外观属性,FillArraysData() 会在创建前自动扫一遍终端文件系统并填好主数组,使用者基本无感。 地址条是第一个被画出来的部件,类型是 OBJ_BITMAP_LABEL,整条都是画布重绘出来的。它的 Y 轴文字位置取地址条高度除以 2,X 轴左缩进固定 5 像素,锚点用左侧纵向居中,这样文字始终落在条幅中线。 首次加载时 m_current_path 还是空串,文件系统元素可能很多,建树耗时间,所以地址条先显示“正在载入,请稍候...”这类提示,再更新画布。等 CreateTreeView() 跑起来,才在循环里把主数组的项目和参数传给树形视图类,同时给每项配图,并去掉文件夹名末尾的 ‘\’ 字符。 下面这段是 CreateFileNavigator() 的核心:先校验表单指针,再 FillArraysData(),随后算坐标、建地址条、建树形视图,对话框或最小化就隐藏。Border() 和 UpdateAddressBar() 是地址条绘制的两个私有方法,后者用 m_address_bar_y_size/2 定 Y 坐标。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建文件导航器                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFileNavigator::CreateFileNavigator(class="kw">const class="type">long chart_id,class="kw">const class="type">int subwin,class="kw">const class="type">int x,class="kw">const class="type">int y)
  {
class=class="str">"cmt">//--- 如果没有表单指针
   if(::CheckPointer(m_wnd)==POINTER_INVALID)
     {
      ::Print(__FUNCTION__," > 在创建文件导航器之前, "
                           "必须传给它表单的指针: CFileNavigator::WindowPointer(CWindow &object).");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 扫描终端文件系统并在数组中保存数据
   FillArraysData();
class=class="str">"cmt">//--- 初始化变量
   m_id       =m_wnd.LastId()+class="num">1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
   m_x        =x;
   m_y        =y;
class=class="str">"cmt">//--- 到边缘的距离
   CElement::XGap(CElement::X()-m_wnd.X());
   CElement::YGap(CElement::Y()-m_wnd.Y());
class=class="str">"cmt">//--- 创建元件
   if(!CreateAddressBar())
      class="kw">return(class="kw">false);
   if(!CreateTreeView())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 如果窗口是对话框或者是最小化状态,隐藏元件
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
class=class="str">"cmt">//---
   class="kw">return(true);
  }
class CFileNavigator : class="kw">public CElement
  {
class="kw">private:
   class=class="str">"cmt">//--- 绘制地址条的边框
   class="type">void               Border(class="type">void);
   class=class="str">"cmt">//--- 在地址条中显示当前路径
   class="type">void               UpdateAddressBar(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在地址条中显示当前路径                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFileNavigator::UpdateAddressBar(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x=class="num">5;
   class="type">int y=m_address_bar_y_size/class="num">2;
class=class="str">"cmt">//--- 清空背景
   m_address_bar.Erase(::ColorToARGB(m_address_bar_back_color,class="num">0));
class=class="str">"cmt">//--- 绘制背景边框
   Border();
class=class="str">"cmt">//--- 文字属性
   m_address_bar.FontSet("Calibri",class="num">14,FW_NORMAL);
class=class="str">"cmt">//--- 如果路径没有设置,显示默认字符串

「地址条与树视图的实例化细节」

文件导航器在 MT5 自定义面板里不是凭空画的,地址条先靠 CreateBitmapLabel 按计算出的宽高生成位图标签,再 Attach 到指定子窗口。宽度逻辑值得盯一眼:当 m_content_area_width 小于 0 时直接取树视图区宽度,大于 0 则拼成 treeview 宽加内容区宽减 1,等于 0 就贴到窗口右缘留 2 像素边距。 代码里 UpdateAddressBar() 会在创建末尾被调用,如果 m_current_full_path 还是空串,它会先填“正在载入,请稍候...”再走 TextOut 输出,用 TA_LEFT|TA_VCENTER 做左对齐垂直居中。之后 m_address_bar.Update() 把画布推到前台,这一步漏掉地址条就不会刷新。 树视图 CreateTreeView 则把窗口指针、ID、尺寸全从 CElement 继承过来,ResizeListAreaMode(true) 配合 TreeViewAreaWidth / ContentAreaWidth 决定可拖拽分区。构建时按 m_g_item_text 数组大小循环塞节点,数组长度就是条目总数,开 MT5 把这两个方法挂到自己的 CFileNavigator 派生类里就能直接验证布局。 外汇与贵金属图表上挂这类 GUI 高风险,面板卡死可能错过报价,建议先在模拟盘子窗口试跑。

MQL5 / C++
if(m_current_full_path=="")
   m_current_full_path="正在载入,请稍候...";
class=class="str">"cmt">//--- 输出文件导航器的地址条路径
   m_address_bar.TextOut(x,y,m_current_path,::ColorToARGB(m_address_bar_text_color),TA_LEFT|TA_VCENTER);
class=class="str">"cmt">//--- 更新绘制的画布
   m_address_bar.Update();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建地址条                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFileNavigator::CreateAddressBar(class="type">void)
  {
class=class="str">"cmt">//--- 构造对象名称
   class="type">class="kw">string name=CElement::ProgramName()+"_file_navigator_address_bar_"+(class="type">class="kw">string)CElement::Id();
class=class="str">"cmt">//--- 坐标
   class="type">int x =CElement::X();
   class="type">int y =CElement::Y();
class=class="str">"cmt">//--- 大小:
class=class="str">"cmt">//   计算宽度
   class="type">int x_size=class="num">0;
class=class="str">"cmt">//--- 如果没有内容区域
   if(m_content_area_width<class="num">0)
      x_size=m_treeview_area_width;
   else
     {
   class=class="str">"cmt">//--- 如果定义了指定的内容区域宽度
      if(m_content_area_width>class="num">0)
         x_size=m_treeview_area_width+m_content_area_width-class="num">1;
   class=class="str">"cmt">//--- 如果内容区域的右边必须在表单的右侧边缘
      else
         x_size=m_wnd.X2()-x-class="num">2;
     }
class=class="str">"cmt">//--- 高度
   class="type">int y_size=m_address_bar_y_size;
class=class="str">"cmt">//--- 创建对象
   if(!m_address_bar.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,x_size,y_size,COLOR_FORMAT_XRGB_NOALPHA))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 附加到图表
   if(!m_address_bar.Attach(m_chart_id,name,m_subwin,class="num">1))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 设置属性
   m_address_bar.Background(class="kw">false);
   m_address_bar.Z_Order(m_zorder);
   m_address_bar.Tooltip("\n");
class=class="str">"cmt">//--- 保存大小
   CElement::X(x);
   CElement::Y(y);
class=class="str">"cmt">//--- 保存大小
   CElement::XSize(x_size);
   CElement::YSize(y_size);
class=class="str">"cmt">//--- 到边缘的距离
   m_address_bar.XGap(x-m_wnd.X());
   m_address_bar.YGap(y-m_wnd.Y());
class=class="str">"cmt">//--- 更新地址条
   UpdateAddressBar();
class=class="str">"cmt">//--- 保存对象指针
   CElement::AddToArray(m_address_bar);
class=class="str">"cmt">//--- 如果窗口是对话框或者是最小化状态,隐藏元件
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      m_address_bar.Timeframes(OBJ_NO_PERIODS);
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建树形视图                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFileNavigator::CreateTreeView(class="type">void)
  {
class=class="str">"cmt">//--- 保存窗口指针
   m_treeview.WindowPointer(m_wnd);
class=class="str">"cmt">//--- 设置属性
   m_treeview.Id(CElement::Id());
   m_treeview.XSize(CElement::XSize());
   m_treeview.YSize(CElement::YSize());
   m_treeview.ResizeListAreaMode(true);
   m_treeview.TreeViewAreaWidth(m_treeview_area_width);
   m_treeview.ContentAreaWidth(m_content_area_width);
class=class="str">"cmt">//--- 构建树形视图的数组
   class="type">int items_total=::ArraySize(m_g_item_text);
   for(class="type">int i=class="num">0; i<items_total; i++)
     {

◍ 树形视图里文件夹尾斜杠的清理逻辑

在把文件浏览器项目塞进 MT5 自定义树形视图控件时,文件夹和文件用的是两套图标:m_g_is_folder[i] 为真就取 m_folder_icon,否则取 m_file_icon,这一行直接决定了节点左边显示什么。 文件夹文本在内部拼接时末尾带了一个 '\' 字符,如果不处理,树里会显示出多余的路径分隔符。代码用 StringSubstr 截掉最后一位——长度是 StringLen 减 1,相当于只保留除尾斜杠外的内容。 AddItem 调用把节点挂到上一级 m_g_prev_node_list_index[i] 下面,并带入层级 m_g_node_level[i]、累计项数等参数;false 表示初始不展开。最后 CreateTreeView 用图表 ID、子窗口号和坐标 (m_x, m_y+m_address_bar_y_size) 真正绘制,失败直接返回 false,成功返回 true。 开 MT5 把这段接进你自己的 CTreeView 封装类,改 m_folder_icon / m_file_icon 路径即可直观验证图标切换与尾斜杠消除效果,外汇与贵金属图表插件开发属高风险定制,参数错误可能导致子窗口渲染异常。

MQL5 / C++
class=class="str">"cmt">//--- 设置项目的图标 (文件夹/文件)
   class="type">class="kw">string icon_path=(m_g_is_folder[i])? m_folder_icon : m_file_icon;
   class=class="str">"cmt">//--- 如果是文件夹,在字符串中删除最后的字符 (&class="macro">#x27;\&class="macro">#x27;) 
   if(m_g_is_folder[i])
      m_g_item_text[i]=::StringSubstr(m_g_item_text[i],class="num">0,::StringLen(m_g_item_text[i])-class="num">1);
   class=class="str">"cmt">//--- 在树形视图中增加项目u
      m_treeview.AddItem(i,m_g_prev_node_list_index[i],m_g_item_text[i],icon_path,m_g_item_index[i],
                         m_g_node_level[i],m_g_prev_node_item_index[i],m_g_items_total[i],m_g_folders_total[i],class="kw">false,m_g_is_folder[i]);
   }
class=class="str">"cmt">//--- 创建树形视图
   if(!m_treeview.CreateTreeView(m_chart_id,m_subwin,m_x,m_y+m_address_bar_y_size))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//---
   class="kw">return(true);
   }

树形视图选路事件的接管逻辑

文件导航器只监听 ON_CHANGE_TREE_PATH 这一个事件 ID,它由用户在树形视图里点选节点时触发。类内部用三个栏位分别存:带盘符的文件系统全路径、终端沙盒相对路径、以及目录区域标记(公用或本地),对外则暴露 CurrentPath()、CurrentFullPath()、DirectoryArea() 和 SelectedFile() 四个取数接口。 事件进来后先拿树视图里的全路径,再用 StringFind 判断是否命中 "Common\\Files\\" 子串(返回值 > -1 即公用域)。命中后通过 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 取公用根目录,并截掉前 7 个字符的 "Common\\" 前缀来拼出长短两种路径,同时写 m_directory_area 标记区域。 下面这段是类的声明与处理函数骨架,注意 SelectedFile() 直接转发了树视图的 SelectedItemFileName(),没有自己做缓存——这意味着每次取选中文件都会穿透到底层控件。外汇与贵金属脚本涉及文件读写,MT5 沙盒权限限制严格,误写绝对路径可能静默失败,调试时建议先打印 CurrentFullPath() 核对。

MQL5 / C++
<span class="keyword">class</span> CFileNavigator : <span class="keyword">class="kw">public</span> CElement
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 当前路径相对于终端"沙盒"的路径</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_current_path;</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 相对于文件系统的当前路径,包含硬盘卷标</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_current_full_path;</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 当前目录区域</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">164, class="num">192, class="num">228);"><span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; m_directory_area;</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 返回 (class="num">1) 当前路径 (class="num">2) 完整路径 </span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CurrentPath(<span class="keyword">class="type">void</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_current_path);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CurrentFullPath(<span class="keyword">class="type">void</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_current_full_path);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 返回 (class="num">1) 目录区域 (class="num">2) 选中的文件</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; DirectoryArea(<span class="keyword">class="type">void</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_directory_area);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">249, class="num">204, class="num">202);"><span class="keyword">class="type">class="kw">string</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SelectedFile(<span class="keyword">class="type">void</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_treeview.SelectedItemFileName()); }</span>
&nbsp;&nbsp;};
<span class="keyword">class</span> CFileNavigator : <span class="keyword">class="kw">public</span> CElement
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 处理在树形视图中选择新路径的事件</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OnChangeTreePath(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 处理在树形视图中选择新路径的事件             &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CFileNavigator::OnChangeTreePath(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- 取得当前路径</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> path=m_treeview.CurrentFullPath();
<span class="comment">class=class="str">"cmt">//--- 如果这是终端公用文件夹</span>
&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">if</span>(::<span class="functions">StringFind</span>(path,<span class="class="type">class="kw">string">"Common\\Files\\"</span>,<span class="number">class="num">0</span>)&gt;-<span class="number">class="num">1</span>)</span>
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 取得终端公用文件夹的地址</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span style="background-class="type">color:rgb(class="num">164, class="num">192, class="num">228);"><span class="keyword">class="type">class="kw">string</span> common_path=::<span class="functions">TerminalInfoString</span>(<span class="macro">TERMINAL_COMMONDATA_PATH</span>);</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 删除字符串中的 "Common\" 前缀 (在事件中得到的)</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; path=::<span class="functions">StringSubstr</span>(path,<span class="number">class="num">7</span>,::<span class="functions">StringLen</span>(common_path)-<span class="number">class="num">7</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 生成路径 (短的和完整的版本 )</span>

「区分共享目录与本地目录的路径解析」

在文件导航器里切换节点时,先要判定当前选中的路径属于哪种存储区域。若路径字符串从索引 0 开始就包含 'MQL5\Files\Common\'(共 6 个字符前缀),说明落在终端共享目录,此时目录区域常量置为 FILE_COMMON,短路径从原 path 第 6 位截取到末尾。 如果匹配的是 'MQL5\Files\'(前缀长 11 个字符)而非 Common 分支,则走本地数据目录逻辑:用 TerminalInfoString(TERMINAL_DATA_PATH) 拿到本地根地址,短路径从第 11 位截取,目录区域标记填 0。两类情况都拼出 m_current_full_path 供后续读写,并调用 UpdateAddressBar() 刷新地址条。 事件侧只监听自定义事件 CHARTEVENT_CUSTOM+ON_CHANGE_TREE_PATH,命中后转交 OnChangeTreePath() 处理并直接返回。你在 MT5 里接这段逻辑时,重点核对 StringSubstr 的起始位——写错一位,文件就会存到错误的沙箱目录。外汇与贵金属相关的文件读写同样受平台沙箱限制,误操作可能导致策略历史丢失,属高风险环节。

MQL5 / C++
   m_current_path      =::StringSubstr(path,class="num">6,::StringLen(path)-class="num">6);
   m_current_full_path =common_path+"\\"+path;
   class=class="str">"cmt">//--- 保存目录区域
   m_directory_area=FILE_COMMON;
  }
class=class="str">"cmt">//--- 如果这是终端的本地目录
   else if(::StringFind(path,"MQL5\\Files\\",class="num">0)>-class="num">1)
   {
   class=class="str">"cmt">//--- 取得终端中本地数据目录的地址
    class="type">class="kw">string local_path=::TerminalInfoString(TERMINAL_DATA_PATH);
   class=class="str">"cmt">//--- 生成路径 (短的和完整的版本 )
   m_current_path      =::StringSubstr(path,class="num">11,::StringLen(path)-class="num">11);
   m_current_full_path =local_path+"\\"+path;
   class=class="str">"cmt">//--- 保存目录区域
   m_directory_area=class="num">0;
   }
class=class="str">"cmt">//--- 在地址条上显示当前路径 
  UpdateAddressBar();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFileNavigator::OnEvent(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)
  {
class=class="str">"cmt">//--- 处理 "树形视图中路径改变" 事件
  if(id==CHARTEVENT_CUSTOM+ON_CHANGE_TREE_PATH)
    {
    OnChangeTreePath();
    class="kw">return;
    }
  }

◍ 把文件导航器塞进界面容器类

要让自定义文件导航器在 MT5 图形界面里真正跑起来,得把它挂到库的总容器 CWndContainer 上。这个类的源码在 WndContainer.mqh,库里所有控件都统一在这里登记。 具体要补三处:文件导航器的私有指针数组、按窗口索引取导航器数量的方法、以及把导航器指针写进库内部数据库的方法。只改这一层,其余控件逻辑不受影响。 下面这段是裁剪后的 CWndContainer 增量代码,高亮行为新增部分。在 MT5 里新建 mqh 引用 FileNavigator.mqh 后,把这段并进去即可编译验证。 代码逐行拆解:

  • #include "FileNavigator.mqh":引入文件导航器类定义,缺这句编译器不认 CFileNavigator。
  • CFileNavigator *m_file_navigators[];:在 WindowElements 结构里加的动态数组,存每个窗口下的导航器指针。
  • int FileNavigatorsTotal(const int window_index);:公开方法,传窗口序号返回该窗口导航器个数,便于遍历。
  • bool AddFileNavigatorElements(const int window_index,CElement &object);:私有方法,把导航器以 CElement 引用塞进库,返回是否成功。
MQL5 / C++
class="macro">#include "FileNavigator.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用于保存所有界面对象的类                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CWndContainer
  {
class="kw">protected:
   class=class="str">"cmt">//--- 窗口数组
   CWindow            *m_windows[];
   class=class="str">"cmt">//--- 元件数组的结构
   class="kw">struct WindowElements
     {
      class=class="str">"cmt">//--- 文件导航器
      CFileNavigator  *m_file_navigators[];
     };
   class=class="str">"cmt">//--- 每个窗口的元件数组
   WindowElements     m_wnd[];
   class=class="str">"cmt">//---
class="kw">public:
   class=class="str">"cmt">//--- 文件导航器的数量
   class="type">int                FileNavigatorsTotal(class="kw">const class="type">int window_index);
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 把树形视图的指针保存到库中
   class="type">bool               AddFileNavigatorElements(class="kw">const class="type">int window_index,CElement &object);
   };

搭一个能跑的文件导航器原型

把前面文章里的 EA 拿来,从 CProgram 用户类里只留主菜单和状态条,其余元件全删,这是快速验证主文件编辑器内容模式的前提。接着加两个外部参数管导航器的内容和模式,代码里的 FN_BOTH 与 FN_ONLY_FOLDERS 就是用来在运行时切换的。 声明 CFileNavigator 实例后,用 CreateFileNavigator() 定坐标和外观。导航器高度设 10 个点,而类构造函数里默认是 20 像素,两者不是一回事,挂载时得心里有数。距表单左边 2 像素、上边 43 像素的定位由 NAVIGATOR1_GAP_X / Y 两个宏控制。 调用链必须放进创建图形界面的主方法里。下面代码里 TreeViewPointer().VisibleItemsTotal(10) 把树形视图一次显示 10 项,TreeViewAreaWidth(250) 把地址区拉到 250 像素宽,滚动条边框改 clrLightGray,这些都能在 MT5 里直接改数看效果。 事件处理函数里同时打印完整路径和短路径,选中文件就打开读前三行写日志;用 DirectoryArea() 拿目录标志,就能限制可浏览的目录范围。编译挂图后,导航器内容应与你终端文件系统一致,自定义类事件跑通后日志显示正常,说明原型可用。外汇与贵金属 EA 开发属高风险活动,图形原型仅用于功能验证,不代表任何实盘收益。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Program.mqh |
class=class="str">"cmt">//|                       Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- 外部参数
input ENUM_FILE_NAVIGATOR_CONTENT NavigatorContent =FN_BOTH;           class=class="str">"cmt">// 导航器内容
input ENUM_FILE_NAVIGATOR_MODE     NavigatorMode    =FN_ONLY_FOLDERS; class=class="str">"cmt">// 导航器模式
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
  class=class="str">"cmt">//--- 文件导航器
  CFileNavigator      m_navigator;
  class=class="str">"cmt">//---
class="kw">private:
  class=class="str">"cmt">//--- 文件导航器
class="macro">#define NAVIGATOR1_GAP_X(class="num">2)
class="macro">#define NAVIGATOR1_GAP_Y(class="num">43)
   class="type">bool               CreateFileNavigator(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建EA面板                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateExpertPanel(class="type">void)
  {
class=class="str">"cmt">//--- 创建控件表单1
class=class="str">"cmt">//--- 创建控件:
class=class="str">"cmt">//    主菜单
class=class="str">"cmt">//--- 上下文菜单
class=class="str">"cmt">//--- 创建状态栏
class=class="str">"cmt">//--- 创建文件导航器
   if(!CreateFileNavigator())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 重绘图表
   m_chart.Redraw();
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建文件导航器                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateFileNavigator(class="type">void)
  {
class=class="str">"cmt">//--- 保存表单的指针
   m_navigator.WindowPointer(m_window1);
class=class="str">"cmt">//--- 坐标
   class="type">int x=m_window1.X()+NAVIGATOR1_GAP_X;
   class="type">int y=m_window1.Y()+NAVIGATOR1_GAP_Y;
class=class="str">"cmt">//--- 在创建之前设置属性
   m_navigator.TreeViewPointer().VisibleItemsTotal(class="num">10);
   m_navigator.NavigatorMode(NavigatorMode);
   m_navigator.NavigatorContent(NavigatorContent);
   m_navigator.TreeViewAreaWidth(class="num">250);
   m_navigator.AddressBarBackColor(clrWhite);
   m_navigator.AddressBarTextColor(clrSteelBlue);
class=class="str">"cmt">//--- 滚动条属性
   m_navigator.TreeViewPointer().GetScrollVPointer().AreaBorderColor(clrLightGray);
   m_navigator.TreeViewPointer().GetContentScrollVPointer().AreaBorderColor(clrLightGray);

「在文件树里点选后怎么把内容读出来」

自定义文件浏览器控件建好之后,真正产生价值的是事件回调。当用户在树形视图里切换路径或点中某个文件,底层会抛出 CHARTEVENT_CUSTOM + ON_CHANGE_TREE_PATH 事件,我们在 OnEvent 里拦截它就能拿到选中项。 拿到选中文件后先拼出完整路径,再用 FileOpen 以只读文本方式打开。注意代码里 m_navigator.DirectoryArea() 这个标志位决定了文件是落在 MQL5/Files 还是 MQL5/Include 这类区域,搞错区域会直接 INVALID_HANDLE。 打开成功后连续三次 FileReadString 把前三行打到日志,用来快速预览 CSV 或参数文件头。下面这段是事件处理与读取的核心片段。 实盘里这类预览对快速核对 EA 回测参数很有用,但外汇和贵金属杠杆高、滑点随机,预览数据不等于可执行信号,仅作本地文件核查。某次日志显示第三行读出 2,155.66,1028.00,1.04,0.30,0.64,0.24,0.01,2,0,10,10,0,说明前三行已正确解析出数值序列。

MQL5 / C++
class=class="str">"cmt">//--- 创建控件
   if(!m_navigator.CreateFileNavigator(m_chart_id,m_subwin,x,y))
       class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 把控件之阵加到库中
   CWndContainer::AddToElementsArray(class="num">0,m_navigator);
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理函数                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(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)
  {
class=class="str">"cmt">//--- 处理 "在树形视图中改变路径" 的事件
   if(id==CHARTEVENT_CUSTOM+ON_CHANGE_TREE_PATH)
     {
     ::Print(__FUNCTION__," > id: ",id,"; 文件名: ",m_navigator.SelectedFile());
     ::Print(__FUNCTION__," > id: ",id,"; 路径: ",m_navigator.CurrentPath()+m_navigator.SelectedFile());
     ::Print(__FUNCTION__," > id: ",id,"; 完整路径: ",m_navigator.CurrentFullPath()+m_navigator.SelectedFile());
     class=class="str">"cmt">//--- 如果文件被选中,就读取它 (前三行)
     if(m_navigator.SelectedFile()!="")
       {
       class=class="str">"cmt">//--- 构建文件的路径
       class="type">class="kw">string path=m_navigator.CurrentPath()+m_navigator.SelectedFile();
       class=class="str">"cmt">//--- 取得指定文件的句柄
       class="type">int filehandle=::FileOpen(path,FILE_READ|FILE_TXT|FILE_ANSI|m_navigator.DirectoryArea(),&class="macro">#x27;\n&class="macro">#x27;);
       class=class="str">"cmt">//--- 如果得到了句柄,读取前三行
       if(filehandle!=INVALID_HANDLE)
         {
         ::Print(__FUNCTION__," > 打开的文件: ",path);
         ::Print(__FUNCTION__," > 行 class="num">01: ",::FileReadString(filehandle));
         ::Print(__FUNCTION__," > 行 class="num">02: ",::FileReadString(filehandle));
         ::Print(__FUNCTION__," > 行 class="num">03: ",::FileReadString(filehandle));
         }
       class=class="str">"cmt">//--- 关闭文件
       ::FileClose(filehandle);
       }
     ::Print("---");
     }
   }

◍ 回测结果落盘时的日志长什么样

EA 在 MT5 优化跑完后会把逐行指标写进 CSV,上面这段是某次实跑日志的截片:2016.06.16 02:15:29.994 这一刻,程序向 DATA_OPTIMIZATION\WriteResOptByCriterion\optimization_results2.csv 写入了第 02 行数据,数值串为 1,260.67,498.00,1.13,1.05,0.26,1.00,0.03,3,0,10,10,0。 紧挨着的上一条日志先输出了表头:№,PROFIT,TOTAL DEALS,PROFIT FACTOR,EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,AmountBars,TakeProfit,StopLoss,TrailingSL,ReversePosition。对照看,第 02 行里净利润 1260.67、总成交 498 笔、盈利因子 1.13、期望收益 1.05、最大回撤 0.26%、恢复因子 1.00、夏普 0.03,后面 3,0,10,10,0 分别对应 K 线数、止盈、止损、追踪止损、反向开关。 文件实际落在 C:\Users\tol64\AppData\Roaming\MetaQuotes\Terminal\Common\Files\ 下,日志里 id:1023 重复出现了三次,分别给出完整路径、相对路径和纯文件名。开 MT5 跑自己的优化时,直接去终端「文件」目录翻 Common\Files 下的同名 CSV,就能拿到一模一样的原始指标矩阵,外汇与贵金属品种回测波动大,这类落盘数据仅代表历史样本,实盘表现可能明显偏离。

别急着下结论

这一阶段的图形界面库已经把静态日历、下拉日历、树形视图、鼠标箭头和文件导航器都收进了结构里,图 3 展示的正是当前开发阶段的整体分层。第八部分到此收口,但库本身还在迭代——作者提到第七版库随新文放出,早期文章里的测试文件已和最新版不兼容。 想直接看效果的人,去下载包里的 Test 文件夹,丢进 MetaTrader 5\MQL5\Experts\ 后打开 Test.mq5 编译即可,不用自己从头拼 OOP 框架。下一步第九部分会补上颜色选择器、进度条和线形图表,届时段落里的控件清单还会再扩。 做 GUI 面板前先确认你拉的是第 7 版库,老教程里的 mqh 直接套容易编译报错。外汇和贵金属界面自动化虽能省手工操作,但终端脚本故障或逻辑漏洞可能错单,实盘前务必在策略测试器跑通。

让小布替你跑这套集成测试
把控件编译进库后那些重复的点路径、看树形展开的机械验证,小布盯盘的 AIGC 已内置类似诊断逻辑,打开对应品种页即可看到界面元素加载状态,你只管确认交互是否符合策略需求。

常见问题

出于安全控制,MQL5 处理的文件只能位于客户端目录的 MQL5\Files 或公用目录的 Common\Files,使用 FILE_COMMON 标志可切换,但无法脱离沙箱访问任意磁盘路径,这是语言层硬限制。
目前小布内置的是界面元素诊断与加载状态查看,不直接产出 MQL5 导航器源码,但可对照本篇结构快速核对你本地库的集成是否缺了事件处理函数绑定。
FN_BOTH 同时在树形视图暴露本地 MQL 文件夹与公用 Common 文件夹,便于跨终端文件选取;单目录模式缩小根节点,适合只操作用户私有数据的轻量工具。
地址条读取树形视图当前选中项的完整相对路径并渲染出来,选中变更时触发事件处理函数同步更新,保证 GUI 上永远显示沙箱内的真实位置。
跟不太住。树形视图与日历等基类在第八部分前两章,关于 CTreeView 的完整讨论见《图形界面 VIII: 文件导航器控件·基础篇》。