图形界面 VIII: 文件导航器控件 (第三章)·综合运用
(3/3)·从 CFileNavigator 类到库集成测试的完整闭环,错过这篇前面两章等于白搭
遍历文件树时节点如何下沉一层
在 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 文件系统权限受限,误用路径会概率性返回空列表。
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 涉及文件读写,须认清平台文件沙箱限制与账户高风险属性。
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 面板里复用终端文件树。外汇与贵金属脚本涉及文件读写,误操作可能动到实盘环境,务必先在模拟账户验证。
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 坐标。
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 高风险,面板卡死可能错过报价,建议先在模拟盘子窗口试跑。
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 路径即可直观验证图标切换与尾斜杠消除效果,外汇与贵金属图表插件开发属高风险定制,参数错误可能导致子窗口渲染异常。
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() 核对。
<span class="keyword">class</span> CFileNavigator : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 当前路径相对于终端"沙盒"的路径</span> <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> m_current_path;</span> <span class="comment">class=class="str">"cmt">//--- 相对于文件系统的当前路径,包含硬盘卷标</span> <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> m_current_full_path;</span> <span class="comment">class=class="str">"cmt">//--- 当前目录区域</span> <span style="background-class="type">color:rgb(class="num">164, class="num">192, class="num">228);"><span class="keyword">class="type">int</span> m_directory_area;</span> <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--- 返回 (class="num">1) 当前路径 (class="num">2) 完整路径 </span> <span class="keyword">class="type">class="kw">string</span> CurrentPath(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_current_path); } <span class="keyword">class="type">class="kw">string</span> CurrentFullPath(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_current_full_path); } <span class="comment">class=class="str">"cmt">//--- 返回 (class="num">1) 目录区域 (class="num">2) 选中的文件</span> <span class="keyword">class="type">int</span> DirectoryArea(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_directory_area); } <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> SelectedFile(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">const</span> { <span class="keyword">class="kw">return</span>(m_treeview.SelectedItemFileName()); }</span> }; <span class="keyword">class</span> CFileNavigator : <span class="keyword">class="kw">public</span> CElement { <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//--- 处理在树形视图中选择新路径的事件</span> <span class="keyword">class="type">void</span> OnChangeTreePath(<span class="keyword">class="type">void</span>); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 处理在树形视图中选择新路径的事件 |</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>) { <span class="comment">class=class="str">"cmt">//--- 取得当前路径</span> <span class="keyword">class="type">class="kw">string</span> path=m_treeview.CurrentFullPath(); <span class="comment">class=class="str">"cmt">//--- 如果这是终端公用文件夹</span> <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>)>-<span class="number">class="num">1</span>)</span> { <span class="comment">class=class="str">"cmt">//--- 取得终端公用文件夹的地址</span> <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> <span class="comment">class=class="str">"cmt">//--- 删除字符串中的 "Common\" 前缀 (在事件中得到的)</span> 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>); <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 的起始位——写错一位,文件就会存到错误的沙箱目录。外汇与贵金属相关的文件读写同样受平台沙箱限制,误操作可能导致策略历史丢失,属高风险环节。
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 引用塞进库,返回是否成功。
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 开发属高风险活动,图形原型仅用于功能验证,不代表任何实盘收益。
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,说明前三行已正确解析出数值序列。
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 直接套容易编译报错。外汇和贵金属界面自动化虽能省手工操作,但终端脚本故障或逻辑漏洞可能错单,实盘前务必在策略测试器跑通。