void AreaBackColor(class="kw">const""> void AreaBackColor(class="kw">const"">
图形界面 VIII: 文件导航器控件 (第三章)·进阶篇
📘

图形界面 VIII: 文件导航器控件 (第三章)·进阶篇

第 2/3 篇

◍ 文件导航控件的配色与图标初始化

在 MT5 自定义 GUI 里封装文件浏览器类时,第一批 setter 往往决定界面可读性。下面这组方法把背景、边框、地址条文字色拆开赋值,调用方可以单独改树形区底色而不碰地址条。

MQL5 / C++
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; }
逐行拆解:AreaBackColor 接收 color 类型参数写入成员变量 m_area_color,控制主显示区底色;AreaBorderColor 同理只改边框色 m_area_border_color。AddressBarBackColor 与 AddressBarTextColor 分别接管地址条背景与文字,避免整体刷色导致对比度失控。FileIcon / FolderIcon 接收字符串路径,把 bmp 图标资源绑到文件与文件夹节点。 构造函数里给出了默认落点:地址条高度 m_address_bar_y_size 固定 20 像素,左侧树形区 m_treeview_area_width 预设 300 像素,内容区宽度先置 0 等后续计算。边框色用 clrLightGray,地址条底 clrWhiteSmoke、文字 clrBlack,文件图标默认指向 Images\EasyAndFastGUI\Icons\bmp16\text_file.bmp。
MQL5 / C++
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"),
#resource 预编译指令把两张 16x16 bmp 编进 ex5,运行时不依赖外部文件。开 MT5 建个 EA 把这段抄进类定义,改 m_treeview_area_width 从 300 到 200,能直接看到左侧目录栏变窄、右侧留白增加,外汇与贵金属图表上挂这类面板需注意过度绘制可能拖慢老机型。

MQL5 / C++
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),若编译通过且无运行时报错,即说明对象链完整。

MQL5 / C++
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() 扫区写主数组。 下面这段是主数组与辅助数组的字段声明,逐行对应前面说的存储结构:

MQL5 / C++
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 峰值再定初始容量。

MQL5 / C++
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,调参时先确认搜索域。

MQL5 / C++
  ::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 沙箱路径限制,误操作有覆盖实盘配置的高风险。

MQL5 / C++
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);
  }

常见问题

在控件初始化阶段为每个层级预设配色方案和图标资源,层级越深用越淡的色调区分,避免视觉混淆。
把所有子对象组合放在一个统一的构造入口函数里初始化,主程序只调用一次,后续靠分级数组驱动刷新。
可以,小布能根据你说出的控件需求直接生成导航器类的对象组合代码和配色初始化片段,省去手动拼装。
展开节点前先判定目录并按需成倍扩容分级数组,同时记录父节点索引,方便回溯不丢路径。
遍历分级数组统计同父节点的目录项数量,回溯时直接取该节点的父索引切回上层即可。