图形界面 VIII: 文件导航器控件 (第三章)·进阶篇
◍ 文件导航控件的配色与图标初始化
在 MT5 自定义 GUI 里封装文件浏览器类时,第一批 setter 往往决定界面可读性。下面这组方法把背景、边框、地址条文字色拆开赋值,调用方可以单独改树形区底色而不碰地址条。
class="type">void AreaBackColor(class="kw">const class="type">color clr) { m_area_color=clr; } class="type">void AreaBorderColor(class="kw">const class="type">color clr) { m_area_border_color=clr; } class=class="str">"cmt">//--- (class="num">1) 背景 (class="num">2) 地址条文字 的颜色 class="type">void AddressBarBackColor(class="kw">const class="type">color clr) { m_address_bar_back_color=clr; } class="type">void AddressBarTextColor(class="kw">const class="type">color clr) { m_address_bar_text_color=clr; } class=class="str">"cmt">//--- 设置图片文件路径,指向 (class="num">1) 文件 和 (class="num">2) 文件夹 class="type">void FileIcon(class="kw">const class="type">class="kw">string file_path) { m_file_icon=file_path; } class="type">void FolderIcon(class="kw">const class="type">class="kw">string file_path) { m_folder_icon=file_path; }
class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\folder.bmp" class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\text_file.bmp" CFileNavigator::CFileNavigator(class="type">void) : m_address_bar_y_size(class="num">20), m_treeview_area_width(class="num">300), m_content_area_width(class="num">0), m_navigator_content(FN_ONLY_MQL), m_area_border_color(clrLightGray), m_address_bar_back_color(clrWhiteSmoke), m_address_bar_text_color(clrBlack), m_file_icon("Images\\EasyAndFastGUI\\Icons\\bmp16\\text_file.bmp"),
class="type">void AreaBackColor(class="kw">const class="type">color clr) { m_area_color=clr; } class="type">void AreaBorderColor(class="kw">const class="type">color clr) { m_area_border_color=clr; } class=class="str">"cmt">//--- (class="num">1) 背景 (class="num">2) 地址条文字 的颜色 class="type">void AddressBarBackColor(class="kw">const class="type">color clr) { m_address_bar_back_color=clr; } class="type">void AddressBarTextColor(class="kw">const class="type">color clr) { m_address_bar_text_color=clr; } class=class="str">"cmt">//--- 设置图片文件路径,指向 (class="num">1) 文件 和 (class="num">2) 文件夹 class="type">void FileIcon(class="kw">const class="type">class="kw">string file_path) { m_file_icon=file_path; } class="type">void FolderIcon(class="kw">const class="type">class="kw">string file_path) { m_folder_icon=file_path; } class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\folder.bmp" class="macro">#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\text_file.bmp" CFileNavigator::CFileNavigator(class="type">void) : m_address_bar_y_size(class="num">20), m_treeview_area_width(class="num">300), m_content_area_width(class="num">0), m_navigator_content(FN_ONLY_MQL), m_area_border_color(clrLightGray), m_address_bar_back_color(clrWhiteSmoke), m_address_bar_text_color(clrBlack), m_file_icon("Images\\EasyAndFastGUI\\Icons\\bmp16\\text_file.bmp"),
文件导航器类的对象组合与构造入口
在 EasyAndFastGUI 体系里,文件导航器不是单一控件,而是由地址栏画布与树形视图组合而成的复合元件。基类 CElement 负责通用属性,派生类 CFileNavigator 通过聚合 CRectCanvas 与 CTreeView 两个成员来实现界面骨架。 构造函数中先调用 CElement::ClassName 保存类标识,再把 m_zorder 设为 0,意味着该元件在鼠标左键点击层级里处于默认最底层,不会被其他浮层抢走事件。 头文件部分除了引入 Element.mqh 和 Window.mqh,还显式包含了 TreeView.mqh,说明树形视图的依赖必须单独编译进项目,漏掉会直接报找不到 CTreeView 符号。 对外只暴露一个 CreateFileNavigator 方法,接收图表 ID、子窗口号以及 x、y 坐标;内部私有方法 CreateAddressBar 与 CreateTreeView 分别负责两块子区域的实例化。想在 MT5 里验证,直接新建 EA 包含该头文件并调用 CreateFileNavigator(0,0,10,10),若编译通过且无运行时报错,即说明对象链完整。
m_folder_icon("Images\\EasyAndFastGUI\\Icons\\bmp16\\folder.bmp") { class=class="str">"cmt">//--- 在基类中保存元件类的名称 CElement::ClassName(CLASS_NAME); class=class="str">"cmt">//--- 设置鼠标左键点击的优先级 m_zorder=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| FileNavigator.mqh | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Element.mqh" class="macro">#include "Window.mqh" class="macro">#include "TreeView.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于创建文件导航器的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 用于创建元件的对象 CRectCanvas m_address_bar; CTreeView m_treeview; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 用于创建文件导航器的方法 class="type">bool 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">//--- class="kw">private: class="type">bool CreateAddressBar(class="type">void); class="type">bool CreateTreeView(class="type">void); };
「分级数组怎么撑起文件树」
做文件导航器之前,得先把终端文件系统扫一遍,把建树需要的参数存进数组。主数组用 g 前缀,记录总索引、父节点索引、名称、局部索引、节点等级、父节点局部索引、目录下元素数、子文件夹数、是否文件夹、展开状态这 10 类字段;辅助数组用 l 前缀,按节点等级动态伸缩。 AuxiliaryArraysResize() 按当前节点等级改辅助数组大小:等级 0 时数组长 1,等级 1 时长 2,依此类推。节点下钻或回退时等级变,数组跟着变。AddItem() 每次往主数组尾部加一个元素并写入传入参数。 判断元素是文件夹还是文件,用 IsFolder(),名字里带 '\' 返回 true;也可用系统函数 FileIsExist(),若搜到的是目录会报 ERR_FILE_IS_DIRECTORY。ItemsTotal() 与 FoldersTotal() 分别统计目录下元素总数和文件夹数,后者只在遇到文件夹时计数器加一,两者都接路径和搜索范围(公用或本地)两个参数,内部用 FileFindFirst() 拿句柄和首个名称,FileFindNext() 循环取后续,结束用 FileFindClose() 关句柄。 PrevNodeItemIndex() 用根目录当前项索引和节点等级算前一节点局部索引。ToNextNode() 负责进文件夹下一级,路径拼接、写辅助数组、取父索引、加主数组、升节点计数器、扩辅助数组、存新等级的路径/元素数/文件夹数,末尾重置局部计数器并关句柄。 主扫描循环在 FileSystemScan() 里:从列表头取句柄和首名存辅助数组;非首索引则局部计数器加一走下一项;越出根列表范围就停循环关句柄;非根到尾就降一级、重置局部计数、关句柄、迭代;遇文件夹调 ToNextNode() 下钻并总索引加一;遇文件则取父局部索引、加主数组、双计数器加一、取下一元素。 FillArraysData() 串起上述方法:依 ENUM_FILE_NAVIGATOR_CONTENT 定公用/本地目录顺序,FN_BOTH 模式先 Common 后本地,单目录模式只圈起止范围。循环里重置局部计数、按级改辅助数组、首索引存数量、加主数组、升总索引与等级、再改辅助数组,本地目录时修辅助数组首索引的局部与前总索引,最后调 FileSystemScan() 扫区写主数组。 下面这段是主数组与辅助数组的字段声明,逐行对应前面说的存储结构:
class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 保存数据的主数组 class="type">int m_g_list_index[]; class=class="str">"cmt">// 总索引 class="type">int m_g_prev_node_list_index[]; class=class="str">"cmt">// 前一节点的总索引 class="type">class="kw">string m_g_item_text[]; class=class="str">"cmt">// 文件/文件夹名称 class="type">int m_g_item_index[]; class=class="str">"cmt">// 局部索引 class="type">int m_g_node_level[]; class=class="str">"cmt">// 节点等级 class="type">int m_g_prev_node_item_index[]; class=class="str">"cmt">// 前一节点的局部索引 class="type">int m_g_items_total[]; class=class="str">"cmt">// 文件夹中元素的数量 class="type">int m_g_folders_total[]; class=class="str">"cmt">// 文件夹中文件夹的数量 class="type">bool m_g_is_folder[]; class=class="str">"cmt">// 文件夹属性 class="type">bool m_g_item_state[]; class=class="str">"cmt">// 项目状态 (收起/打开) class=class="str">"cmt">//--- 用于收集数据的辅助数组
◍ 文件树展开时的数组扩容逻辑
在 MT5 自定义文件导航控件里,每一层目录节点都要挂一组平行数组来记录路径、文本和计数。CFileNavigator 私有成员 m_l_prev_node_list_index、m_l_item_text、m_l_path、m_l_item_index、m_l_item_total、m_l_folders_total 就是按 node_level 维度拉的六条辅助数组。 AuxiliaryArraysResize 接收 node_level 后,先算 new_size=node_level+1,再用全局域 ::ArrayResize 把六条数组同步拉長一个元素。扩容完立刻对当前层做清零:prev 索引置 0、文本与路径置空串、item_index 置 -1、两个 total 置 0,避免上层残留数据污染新节点。 真正往树里塞内容的入口是 AddItem。它先取 m_g_list_index 当前长度 array_size,新尺寸定为 array_size+1,随后对 m_g_prev_node_list_index 做 ArrayResize。外汇与贵金属 EA 做面板时,这类目录结构若层级失控,数组反复Resize会带来隐性开销,建议先在脚本里打印 node_level 峰值再定初始容量。
class="type">int m_l_prev_node_list_index[]; class="type">class="kw">string m_l_item_text[]; class="type">class="kw">string m_l_path[]; class="type">int m_l_item_index[]; class="type">int m_l_item_total[]; class="type">int m_l_folders_total[]; }; class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 把数组大小增加一个元素 class="type">void AuxiliaryArraysResize(class="kw">const class="type">int node_level); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 把数组大小额外增加一个元素 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFileNavigator::AuxiliaryArraysResize(class="kw">const class="type">int node_level) { class="type">int new_size=node_level+class="num">1; ::ArrayResize(m_l_prev_node_list_index,new_size); ::ArrayResize(m_l_item_text,new_size); ::ArrayResize(m_l_path,new_size); ::ArrayResize(m_l_item_index,new_size); ::ArrayResize(m_l_item_total,new_size); ::ArrayResize(m_l_folders_total,new_size); class=class="str">"cmt">//--- 初始化最新值 m_l_prev_node_list_index[node_level] =class="num">0; m_l_item_text[node_level] =""; m_l_path[node_level] =""; m_l_item_index[node_level] =-class="num">1; m_l_item_total[node_level] =class="num">0; m_l_folders_total[node_level] =class="num">0; } class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 把项目加到数组中 class="type">void AddItem(class="kw">const class="type">int list_index,class="kw">const class="type">class="kw">string item_text,class="kw">const class="type">int node_level,class="kw">const class="type">int prev_node_item_index, class="kw">const class="type">int item_index,class="kw">const class="type">int items_total,class="kw">const class="type">int folders_total,class="kw">const class="type">bool is_folder); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 使用指定参数把项目加到数组中 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFileNavigator::AddItem(class="kw">const class="type">int list_index,class="kw">const class="type">class="kw">string item_text,class="kw">const class="type">int node_level,class="kw">const class="type">int prev_node_item_index, class="kw">const class="type">int item_index,class="kw">const class="type">int items_total,class="kw">const class="type">int folders_total,class="kw">const class="type">bool is_folder) { class=class="str">"cmt">//--- 把数组大小增加一个元件 class="type">int array_size =::ArraySize(m_g_list_index); class="type">int new_size =array_size+class="num">1; ::ArrayResize(m_g_prev_node_list_index,new_size);
文件树展开时的数组扩容与目录判定
在 MT5 自定义文件导航类里,每插入一个节点都要先把 8 个并行数组同步扩容。m_g_list_index、m_g_item_text、m_g_item_index、m_g_node_level、m_g_prev_node_item_index、m_g_items_total、m_g_folders_total、m_g_is_folder 用 ArrayResize 统一拉到 new_size,否则越界写会直接抛数组越界错误。 扩容后把传入参数落盘到 array_size 位。注意 m_g_prev_node_list_index 在 node_level==0 时写 -1,表示根节点没有上级链表;非根节点则取 m_l_prev_node_list_index[node_level-1],这层索引链是后续回溯父目录的关键。 IsFolder 的判定极简:用 StringFind(file_name,"\\",0) 看是否含反斜杠,返回值大于 -1 就认作文件夹。实测 MT5 的 FileFindFirst 在 MQL5\Files 下搜 "*" 时,带路径分隔符的条目基本都是子目录,这套逻辑足够区分一二级结构。 ItemsTotal 用 FileFindFirst 拿 search_handle,非空就 counter++ 起步。search_area 传错(比如用 FILE_COMMON 当本地目录)会返回 INVALID_HANDLE,counter 恒为 0,调参时先确认搜索域。
::ArrayResize(m_g_list_index,new_size); ::ArrayResize(m_g_item_text,new_size); ::ArrayResize(m_g_item_index,new_size); ::ArrayResize(m_g_node_level,new_size); ::ArrayResize(m_g_prev_node_item_index,new_size); ::ArrayResize(m_g_items_total,new_size); ::ArrayResize(m_g_folders_total,new_size); ::ArrayResize(m_g_is_folder,new_size); class=class="str">"cmt">//--- 保存传入的参数值 m_g_prev_node_list_index[array_size] =(node_level==class="num">0)? -class="num">1 : m_l_prev_node_list_index[node_level-class="num">1]; m_g_list_index[array_size] =list_index; m_g_item_text[array_size] =item_text; m_g_item_index[array_size] =item_index; m_g_node_level[array_size] =node_level; m_g_prev_node_item_index[array_size] =prev_node_item_index; m_g_items_total[array_size] =items_total; m_g_folders_total[array_size] =folders_total; m_g_is_folder[array_size] =is_folder; } class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 确定传入的名称是文件还是文件夹 class="type">bool IsFolder(class="kw">const class="type">class="kw">string file_name); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 确定传入的名称是文件还是文件夹 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CFileNavigator::IsFolder(class="kw">const class="type">class="kw">string file_name) { class=class="str">"cmt">//--- 如果名称包含 "\\" 字符, 它就是一个文件夹 if(::StringFind(file_name,"\\",class="num">0)>-class="num">1) class="kw">return(true); class=class="str">"cmt">//--- class="kw">return(class="kw">false); } class CFileNavigator : class="kw">public CElement { class="kw">private: class=class="str">"cmt">//--- 返回指定目录中 (class="num">1) 项目 和 (class="num">2) 文件夹的数量 class="type">int ItemsTotal(class="kw">const class="type">class="kw">string search_path,class="kw">const class="type">int mode); class="type">int FoldersTotal(class="kw">const class="type">class="kw">string search_path,class="kw">const class="type">int mode); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算当前目录的文件数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CFileNavigator::ItemsTotal(class="kw">const class="type">class="kw">string search_path,class="kw">const class="type">int search_area) { class="type">int counter =class="num">0; class=class="str">"cmt">// 项目计数器 class="type">class="kw">string file_name =""; class=class="str">"cmt">// 文件名称 class="type">long search_handle =INVALID_HANDLE; class=class="str">"cmt">// 搜索句柄 class=class="str">"cmt">//--- 取得当前目录的第一个文件 search_handle=::FileFindFirst(search_path,file_name,search_area); class=class="str">"cmt">//--- 如果目录不是空的 if(search_handle!=INVALID_HANDLE && file_name!="") { class=class="str">"cmt">//--- 计算当前目录中的对象数量 counter++;
「在 MT5 文件树里清点目录与回溯节点」
CFileNavigator 的 FoldersTotal 方法只做一件事:遍历指定 search_path 与 search_area 下的条目,用 IsFolder 判定后累加文件夹数。它先以 FileFindFirst 拿句柄,循环中 FileFindNext 推进,结束必调 FileFindClose 释放句柄——漏掉这一行,MT5 终端的文件搜索句柄会持续占用,多轮调用后可能触发资源告警。 下面这段是 FoldersTotal 的核心循环,注意 counter 只在 IsFolder 为真时自增,因此返回值是纯文件夹计数而非文件总数: while(::FileFindNext(search_handle,file_name)) { if(IsFolder(file_name)) counter++; } ::FileFindClose(search_handle); return(counter); PrevNodeItemIndex 则负责根据 node_level 与 root_index 回推上一级局部索引。当 node_level>1 直接取 m_l_item_index[node_level-1];根层且 root_index>0 时也走同一数组位,其余情况返回 0。这意味着根目录首项的前驱索引恒为 0,构建树形导航时不要对它做越界访问。 开 MT5 自建一个 CFileNavigator 实例,对 MQL5/Files 目录跑一次 FoldersTotal,对比资源管理器里的文件夹数,能快速验证遍历逻辑是否按预期工作。外汇与贵金属相关的文件批处理同样受 MT5 沙箱路径限制,误操作有覆盖实盘配置的高风险。
class="type">int CFileNavigator::FoldersTotal(class="kw">const class="type">class="kw">string search_path,class="kw">const class="type">int search_area) { class="type">int counter =class="num">0; class=class="str">"cmt">// 项目计数器 class="type">class="kw">string file_name =""; class=class="str">"cmt">// 文件名称 class="type">long search_handle=INVALID_HANDLE; class=class="str">"cmt">// 搜索句柄 class=class="str">"cmt">//--- 取得当前目录的第一个文件 search_handle=::FileFindFirst(search_path,file_name,search_area); class=class="str">"cmt">//--- 如果不为空,在循环中计算当前目录的对象数量 if(search_handle!=INVALID_HANDLE && file_name!="") { class=class="str">"cmt">//--- 如果是一个文件夹,增加计数器 if(IsFolder(file_name)) counter++; class=class="str">"cmt">//--- 在列表中迭代,计算其他文件夹 while(::FileFindNext(search_handle,file_name)) { if(IsFolder(file_name)) counter++; } } class=class="str">"cmt">//--- 关闭搜索句柄 ::FileFindClose(search_handle); class="kw">return(counter); } class="type">int CFileNavigator::PrevNodeItemIndex(class="kw">const class="type">int root_index,class="kw">const class="type">int node_level) { class="type">int prev_node_item_index=class="num">0; class=class="str">"cmt">//--- 如果不是根目录 if(node_level>class="num">1) prev_node_item_index=m_l_item_index[node_level-class="num">1]; else { class=class="str">"cmt">//--- 如果不是列表中第一个项目 if(root_index>class="num">0) prev_node_item_index=m_l_item_index[node_level-class="num">1]; } class=class="str">"cmt">//--- 返回前面节点的局部索引 class="kw">return(prev_node_item_index); }