上位机MFC实现文件夹的浏览选择(文件夹按修改时间排序)
引言
在浏览选择文件夹上,微软早已给出 SHBrowseForFolder 方法,但是此方法浏览的文件夹,没有按修改时间排序功能。SHBrowseForFolder 方法会按照名称来进行排序,win7 或者 win10 可能在新式的浏览文件夹下可以选择按修改时间来排序(未尝试),但用着也不是很方便。在 xp 环境下就没有办法用此方法来排序。如果我们要在大量文件夹下选择到最新修改的文件夹,SHBrowseForFolder 方法就无法满足了,因此通过 MFC 编写了一个按时间(修改时间、创建时间、访问时间)排序的浏览文件夹的对话窗口。
文章目录
- 引言
- 一、实现结果
- 二、实现思路
- :one: 建立二叉树
- 1.二叉树节点
- 2.循环生成节点
- :two: 二叉树节点排序
- :three: 节点显示
- :four: 点击事件
- 三、完整代码
- :one: CFileSelect02Dlg
- :two: [想要更完整代码](https://download.csdn.net/download/CSDN_zhuceming/12926406)?
一、实现结果
双击 “我的文件夹” 后:
显示速度以所搜索的文件夹数量来定,设定目录下文件夹(包括子文件夹、子目录)过多势必会减慢速度。
排序的顺序和 win10 自身用修改日期的顺序一致,会将你设定的目录下的所有文件夹(注意是文件夹!!,此文是针对文件夹的选择,如果想选择文件,对程序修改也可以达到)、子文件夹都进行时间排序。
选择好文件夹点击 “确定” button 后,会弹出提示,显示出这个文件夹的真实地址,方便我们对这个文件夹的后续操作。
二、实现思路
思路很简单,将文件夹从设定好的目录下通过 CFileFind::FindNextFile() 都寻找出来,把这些文件夹建立成二叉树信息,之后我们便只对二叉树来进行移动排序、并且显示出来。
1️⃣ 建立二叉树
首先建立二叉树先要定义树的节点,如何定义较为科学呢?
这里思路是:节点的左子树存储当前文件夹的子文件夹信息,节点的右子树存储同属于父文件夹的 “兄弟文件夹”。
举个栗子:在 D:\\Test\\ 文件夹📁目录下,有三个文件夹,名称分别为 “2019”、“2020”、“2021”(“2019” 📁修改时间最早,“2021” 📁修改时间最晚)。对于这三个文件的父文件夹 “Test” 来说,“Test” 节点的左子树应该为下面子文件夹的 ”2021“ (2021 是最近被修改的),对于 “2019”、“2020”、“2021” 这三个文件夹来说,“2021” 节点的右子树应是 “2020”节点,“2020”节点的右子树为 “2019”节点,“2019”节点的右子树就为 nullptr 了。看着下面图就更好理解了:
1.二叉树节点
实际代码结构体:
// 将文件目录层次结构转换成二叉树来操作 每个 文件夹 视为一个节点,文件 不作考虑
typedef struct File_folder
{CString folderName;File_folder* Left_Child; //该节点的孩子放在左子树File_folder* Right_Brother; //该节点的兄弟放在右子树FILETIME LastWriteTime; //文件的修改时间File_folder(){folderName = "";Left_Child = nullptr;Right_Brother = nullptr;}
};
2.循环生成节点
通过循环加递归可以使二叉树生成出来,但是此二叉树是没有排序的二叉树,只是完成了二叉树的 “无” 到 “有” 的地步。其中还需要获得文件的修改时间(可以改为创建时间、访问时间)。
BOOL res = FileFind.FindFile(str_Dir/*查找目录*/);
File_folder * Ff_brother = nullptr;
while (res)
{res = FileFind.FindNextFile();//若目录是文件夹if (FileFind.IsDirectory() && !FileFind.IsDots()){CString strPath = FileFind.GetFilePath(); //得到路径,做为递归调用的开始 CString strTitle = FileFind.GetFileName();//得到目录名,做为树控的结点 // 以下开始进行文件夹信息录取操作// 1.若此文件夹为此级目录下找到的第一个文件夹,则该文件夹的父节点的左子树应置为指向此节点的指针if (isFristChlidNode){Ff = new File_folder();// Ff 节点要置为 Ff_father 节点的孩子节点Ff_father->Left_Child = Ff;Ff->folderName = strTitle;Ff->LastWriteTime = lpLastWriteTime;//若节点要存储文件创建时间信息,可换 lpCreationTime;同理 lpLastAccessTimeFf_brother = Ff; //赋值给兄弟节点方便后面赋值isFristChlidNode = FALSE; //后面的文件就不在是此目录下的第一个节点的,都是此节点的兄弟节点了}// 2.此目录下又找到了第 2 个、3 个、4 个......文件夹,就让他们通过 Right_Brother 指针连接起来else{Ff_temp = new File_folder();Ff_brother->Right_Brother = Ff_temp;Ff_temp->folderName = strTitle;Ff_temp->LastWriteTime = lpLastWriteTime;Ff_brother = Ff_temp; //节点赋值完成,让此节点变成兄弟节点,方便下一次的赋值}CloseHandle(hDir);}
}
FileFind.Close();
2️⃣ 二叉树节点排序
二叉树节点排序的问题我们可以变成链表排序节点的问题,在本例,我们对每个节点的右子树(兄弟节点)来进行排序就行,而节点的左子树(父子节点)就可以无视掉了。所以本二叉树节点的排序就可以视为若干个单链表的排序即可,把 Right_Brother 右指针视为单链表排序中的 Next 指针更好理解。
本例中的链表排序算法用的是归并排序算法,归并排序在链表中的排序表现还不错,时间复杂度为 O(nlog2n) ,空间复杂度O(1)。想要看 sortList 函数在完整代码里。
//排序 head 的兄弟节点
File_folder* TreeToSort(File_folder * head)
{if (head == nullptr)return head;File_folder * new_head;new_head = sortList(head); //sortList 函数为归并排序函数File_folder * p = new_head;while (true){if (p->Left_Child != nullptr)p->Left_Child = TreeToSort(p->Left_Child); //递归循环排序出节点顺序if (p->Right_Brother != nullptr)p = p->Right_Brother; //换下一个兄弟节点elsereturn new_head; //因顺序改变,则返回一个新的节点}
}
3️⃣ 节点显示
节点显示出来,肯定要使用 CTreeCtrl控件了,将CTreeCtrl控件的 Has Lines 与 Has Buttons 都置为 TRUE,这样展示出来的效果能清晰些。
// m_FileTree 为 CTreeCtrl 的控制变量
// 将链表头结点和 Tree 中的某个节点传递进去,此处我们将传递 TreeCtrl 的根节点
void ShowTheTree(File_folder* head, HTREEITEM tree_Root)
{if (head == nullptr)return;CString FileName = head->folderName;if (FileName == "")return;// TreeCtrl 插入一个新节点后,我们可以获得该节点的子节点 tree_childHTREEITEM tree_child = m_FileTree.InsertItem(FileName, 0, 0, tree_Root);if (head->Left_Child != nullptr) // 1.当此节点有孩子节点时,递归显示它的孩子节点ShowTheTree(head->Left_Child, tree_child);if (head->Right_Brother != nullptr) // 2.当此节点有兄弟节点时,递归显示它的兄弟节点ShowTheTree(head->Right_Brother, tree_Root);}
4️⃣ 点击事件
树排序完了,也把文件树结构显示出来了,但光显示出来肯定还不够,弹出浏览文件夹框是为了选择文件夹,是为了对选择的文件夹来进行操作的,所以要得到选择的文件夹的地址才能进行操作的。也是利用递归则解决问题。
//传递一个 HTREEITEM 项,便可返回一个 CString 路径
CString GetFilePathByTreeItem(HTREEITEM item)
{if (item == NULL) return NULL;HTREEITEM TreeRoot = m_FileTree.GetRootItem();if (item == TreeRoot)return NULL;HTREEITEM item_Parent = m_FileTree.GetParentItem(item);// 递归出 Tree 的路径CString str_TreeDir = GetFilePathByTreeItem(item_Parent) + "\\" + m_FileTree.GetItemText(item); return str_TreeDir;
}
此递归只能得到从所选项到 Tree 控件根节点之间的目录,如 “\Text\2021”,所以最后还要加上我们是从哪个目录开始搜索的地址,使之变成如: “D:\Text\2021” 。
三、完整代码
1️⃣ CFileSelect02Dlg
BOOL CFileSelect02Dlg::OnInitDialog()
{CDialogEx::OnInitDialog();// 将“关于...”菜单项添加到系统菜单中。// IDM_ABOUTBOX 必须在系统命令范围内。ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);ASSERT(IDM_ABOUTBOX < 0xF000);CMenu* pSysMenu = GetSystemMenu(FALSE);if (pSysMenu != nullptr){BOOL bNameValid;CString strAboutMenu;bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);ASSERT(bNameValid);if (!strAboutMenu.IsEmpty()){pSysMenu->AppendMenu(MF_SEPARATOR);pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);}}// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动// 执行此操作SetIcon(m_hIcon, TRUE); // 设置大图标SetIcon(m_hIcon, FALSE); // 设置小图标// TODO: 在此添加额外的初始化代码//初始化树形控件 HTREEITEM m_TreeRoot = m_FileTree.InsertItem(_T("我的文件夹"));//插入根节点 TraversalPATH(PATH, &Head); //PATH:以E:\\test为根目录进行遍历,生成以 Head 为根的二叉树Head.Left_Child = TreeToSort(Head.Left_Child);ShowTheTree(Head.Left_Child, m_TreeRoot);return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}//排序 head 的兄弟节点
File_folder * CFileSelect02Dlg::TreeToSort(File_folder * head)
{if (head == nullptr)return head;File_folder * new_head;new_head = sortList(head);File_folder * p = new_head;while (true){if (p->Left_Child != nullptr)p->Left_Child = TreeToSort(p->Left_Child);//递归循环排序出节点顺序if (p->Right_Brother != nullptr)p = p->Right_Brother; //换下一个兄弟节点elsereturn new_head; //因顺序改变,则返回一个新的节点}
}// 遍历PATH目录下的文件夹,变为二叉树
void CFileSelect02Dlg::TraversalPATH(CString str_Dir, File_folder* Ff_father = nullptr)
{if (Ff_father == nullptr)return;CFileFind FileFind;BOOL isFristChlidNode = TRUE; //是否为此级目录第一个节点的标志位,如果当前找到第一个文件夹,则可以创建该节点,//如果该节点的子目录连一个文件夹都没有找到,则也没有必要生成节点,//所以此是否为第一个节点的判断就很重要,若有该节点,则:// 1、说明此该目录级别下,有文件夹,应该创建节点。// 2、若此目录下还有其他文件夹,应变为此文件夹的兄弟节点。// Ff 节点为形参 Ff_father 节点的孩子File_folder *Ff = nullptr, *Ff_temp = nullptr;// HTREEITEM 临时变量,用以记录返回的树节点 HTREEITEM tree_Temp;//判断输入目录最后是否存在'\',不存在则补充 if (str_Dir.Right(1) != "\\")str_Dir += "\\";str_Dir += "*.*";BOOL res = FileFind.FindFile(str_Dir);File_folder * Ff_brother = nullptr;while (res){res = FileFind.FindNextFile();//若目录是文件夹if (FileFind.IsDirectory() && !FileFind.IsDots()){CString strPath = FileFind.GetFilePath(); //得到路径,做为递归调用的开始 CString strTitle = FileFind.GetFileName();//得到目录名,做为树控的结点 FILETIME lpCreationTime; // 文件夹的创建时间FILETIME lpLastAccessTime; // 对文件夹的最近访问时间FILETIME lpLastWriteTime; // 文件夹的最近修改时间// 有 GENERIC_READ 参数才能读取到文件夹时间信息 HANDLE hDir = CreateFile(strPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);GetFileTime(hDir, &lpCreationTime, &lpLastAccessTime, &lpLastWriteTime);// 以下开始进行文件夹信息录取操作// 1.若此文件夹为此级目录下找到的第一个文件夹,则该文件夹的父节点的左子树应置为指向此节点的指针if (isFristChlidNode){Ff = new File_folder();// Ff 节点要置为 Ff_father 节点的孩子节点Ff_father->Left_Child = Ff;Ff->folderName = strTitle;Ff->LastWriteTime = lpLastWriteTime;//若节点要存储文件创建时间信息,可换 lpCreationTime;同理 lpLastAccessTimeFf_brother = Ff; //赋值给兄弟节点方便后面赋值isFristChlidNode = FALSE; //后面的文件就不在是此目录下的第一个节点的,都是此节点的兄弟节点了}// 2.此目录下又找到了第 2 个、3 个、4 个......文件夹,就让他们通过 Right_Brother 指针连接起来else{Ff_temp = new File_folder();Ff_brother->Right_Brother = Ff_temp;Ff_temp->folderName = strTitle;Ff_temp->LastWriteTime = lpLastWriteTime;Ff_brother = Ff_temp;//节点赋值完成,让此节点变成兄弟节点,方便下一次的赋值}CloseHandle(hDir);//递归遍历 Ff_brother 下的目录文件夹TraversalPATH(strPath, Ff_brother);}}FileFind.Close();
}// 将链表头结点和 Tree 中的某个节点传递进去,此处我们将传递 TreeCtrl 的根节点
void CFileSelect02Dlg::ShowTheTree(File_folder* head, HTREEITEM tree_Root)
{if (head == nullptr)return;CString FileName = head->folderName;if (FileName == "")return;// TreeCtrl 插入一个新节点后,我们可以获得该节点的子节点 tree_childHTREEITEM tree_child = m_FileTree.InsertItem(FileName, 0, 0, tree_Root);if (head->Left_Child != nullptr) // 1.当此节点有孩子节点时,递归显示它的孩子节点ShowTheTree(head->Left_Child, tree_child);if (head->Right_Brother != nullptr) // 2.当此节点有兄弟节点时,递归显示它的兄弟节点ShowTheTree(head->Right_Brother, tree_Root);}// 链表通过归并排序,对节点内的 FILETIME 类型时间进行比较
// sortList() 接口:传递头结点,返回排序完成的节点指针,此排序只是排序头结点的兄弟节点
File_folder * CFileSelect02Dlg::sortList(File_folder * head)
{return (head == NULL) ? NULL : mergeSort(head);
}//快慢指针找链表中点
File_folder * CFileSelect02Dlg::findMid(File_folder * head)
{File_folder * slow = head;File_folder * fast = head;File_folder * previous = NULL;while (fast != NULL && fast->Right_Brother != NULL){previous = slow;slow = slow->Right_Brother;fast = fast->Right_Brother->Right_Brother;}// split the list into two partsprevious->Right_Brother = NULL;return slow;
}File_folder * CFileSelect02Dlg::mergeTwoLists(File_folder * l1, File_folder * l2)
{if (l1 == NULL) return l2;if (l2 == NULL) return l1;if (CompareFileTime(&(l1->LastWriteTime), &(l2->LastWriteTime)) == 1) //当为 1 的时候,说明 l2 比 l1 早修改,也就是 l1 是较晚被修改的,那么 l1 就应该比 l2 更靠近父节点{l1->Right_Brother = mergeTwoLists(l1->Right_Brother, l2); //所以对这两个节点来说,l2 应成为 l1 的右子树节点return l1;}else {l2->Right_Brother = mergeTwoLists(l1, l2->Right_Brother);return l2;}
}File_folder * CFileSelect02Dlg::mergeSort(File_folder * head)
{if (head->Right_Brother == NULL) return head;File_folder * mid = findMid(head);File_folder * l1 = mergeSort(head);File_folder * l2 = mergeSort(mid);return mergeTwoLists(l1, l2);
}//传递一个 HTREEITEM 项,便可返回一个 CString 路径
CString CFileSelect02Dlg::GetFilePathByTreeItem(HTREEITEM item)
{if (item == NULL) return NULL;HTREEITEM TreeRoot = m_FileTree.GetRootItem();if (item == TreeRoot)return NULL;HTREEITEM item_Parent = m_FileTree.GetParentItem(item);// 递归出 Tree 的路径CString str_TreeDir = GetFilePathByTreeItem(item_Parent) + "\\" + m_FileTree.GetItemText(item); return str_TreeDir;
}void CFileSelect02Dlg::OnBnClickedBtnOk()
{// TODO: 在此添加控件通知处理程序代码HTREEITEM SelectedItem = m_FileTree.GetSelectedItem();if (SelectedItem != NULL){CString str_CurFilePath = PATH + GetFilePathByTreeItem(SelectedItem); // str_CurFilePath 为节点文件夹的完整路径CFileFind FileFind;BOOL res = FileFind.FindFile(str_CurFilePath);if (res == TRUE){MessageBox(str_CurFilePath);}else{MessageBox(TEXT("文件未能找到!"));}FileFind.Close();}else{MessageBox(TEXT("未选择文件夹"));}
}void CFileSelect02Dlg::OnBnClickedBtnCancel()
{// TODO: 在此添加控件通知处理程序代码CDialogEx::OnCancel();
}
2️⃣ 想要更完整代码?
看完了? 😉
来个赞呗 👍
欢迎评论区批评指正 👇
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
