DirectoryList






3.50/5 (2投票s)
一个自定义列表框控件,
引言
每次使用计算机时,其数据都会为许多独特而有趣的任务进行操作:播放MP3、浏览图片,甚至阅读电子邮件……在所有情况下,这些数据都可以移动、读取、写入、复制等,到存储子系统内的任意目标。令人惊讶的是,所有这一切都在幕后进行,让普通用户享受当今计算机所提供的便利。然而,总会有一个时刻,某些数据不仅对计算机很重要,对用户也很重要。无论一个人的数据是否重要,最终都是主观的,但是如何操作这些数据的问题,是我探索.NET框架如何提供帮助的最初动机。自从我第一次涉足这个项目以来,这条道路漫长而激动人心,这不仅是因为已经提供了许多解决方案,还因为这个问题的广度和复杂性。我写这篇文章的目的是分享我最初遇到的问题、想法和解决方案,以帮助其他人更好地理解.NET框架中操作数据的动态,特别是我是如何利用它来满足我的需求的。
背景
虽然操作数据可以有很多含义,但我最初的问题是创建一个程序,可以轻松地将文件和文件夹备份到用户指定的指定位置。这个项目我称之为 2Backup,它包含一个用于所有添加的文件/文件夹的 ListBox,用于将文件/文件夹添加到 ListBox 和从中移除的按钮,一个目标文本框,以及一个开始备份的复制按钮。这是基本布局的图片。
这个想法是,通过拖放,或者使用添加按钮,将您想要备份的文件或文件夹添加到 ListBox 中。选择目标位置后,选择一个复制模式,然后开始备份。我选择了三种复制模式:
- 目录复制 - 复制所有文件/文件夹,保留源文件夹结构
- 合并文件 - 将所有文件/文件夹复制到一个文件夹中
- CD备份 - 与目录复制相同,但每 700MB 创建一个新文件夹
遇到的第一个问题是如何使用 .NET 框架实现一个将文件和文件夹添加到列表框的函数。我决定使用 OpenFileDialog
,当 `multiSelect` 设置为 `true` 时,它会将所有选定的文件复制到列表框中。这种方法存在一些问题:
- .NET
OpenFileDialog
类在出现错误之前只允许选择有限数量的文件 - 加载文件夹比选择大量文件更容易
注意:如果有人能提供“选择文件过多”错误的解决方法,请随时与我联系。
这两个问题的解决方案是使用FolderBrowser
对话框和 OpenFileDialog
,这样如果用户想快速加载大量文件,添加文件夹是最有效的方式。另一方面,如果他们只想添加一两个文件,他们可以使用 OpenFileDialog
来实现。这个想法得以实现,但添加 FolderBrowser
对话框本身也带来了一些问题:当用户选择一个文件夹时,2Backup 应该只添加该文件夹内的文件,还是应该遍历所有子文件夹并添加它找到的任何文件?
我的解决方案是使用带有参数的 void GetFiles(String* directoryData[], bool subdirsFlag)
函数
String* directoryData[]
- 一个包含所有文件/文件夹/数据的字符串数组bool subdirsFlag
- 一个布尔标志,指示是否应分析子目录
GetFiles(String* directoryData[], bool subdirsFlag)
{
//To guarantee thread safety, lock the ArrayLists
Monitor::Enter(syncfileList);
Monitor::Enter(syncfolderList);
int droppedfoldersIndex = 0;
String* initalFolder;
copy = false;
//To remember the starting point of the mainloop
if(directoryData->Length == 0)
{
initalFolder = "DONE";
}
else
{
initalFolder = directoryData[droppedfoldersIndex];
}
//Initialize mainloop and UpdateUI variables
complete = false;
hasErrored = false;
int folderarrayLength = directoryData->Length;
int loopIndex = syncfolderList->Count;
String* currentItem;
//Begin mainloop
while(droppedfoldersIndex < folderarrayLength)
{
//Starting point
currentItem = directoryData[droppedfoldersIndex];
//Add 1 to folderCount if the currentItem is a folder
if(currentItem->LastIndexOf(".") == -1)
{
folderCount++;
}
//Initialize currentItemInfo loop variables
bool loopComplete = false;
int folderIndex = 0;
//Begin currentItemInfo loop
while(loopComplete == false)
{
try
{
//CASE 1: currentItem is a directory
if (Directory::Exists(currentItem)) //dirInfo->Exists
{
//Get files and folders inside currentItem
String* files[] = Directory::GetFiles(currentItem);
String* subDirs[] =
Directory::GetDirectories(currentItem);
int subDirsLength = subDirs->Length;
int filesLength = files->Length;
//Add files/subDirs into a particular ArrayList
//depending on which case you have
if(folderIndex <= subDirsLength)
{
//CASE 1: subdirsFlag is set false;
//Add only the files of initalDirectory
if(subdirsFlag == false)
{
syncfileList->AddRange(static_cast<ICollection*>
(files->SyncRoot));
//File Count
fileCount = fileCount + filesLength;
loopComplete = true;
}
//CASE 2: 0 files and folders
else if(filesLength == 0 && subDirsLength == 0)
{
//File and Folder Count
fileCount = fileCount + filesLength;
folderCount = folderCount + subDirsLength;
folderIndex++;
}
//CASE 3: Only files
else if(filesLength > 0 && subDirsLength == 0)
{
//Add files
syncfileList->AddRange(static_cast<ICollection*>
(files->SyncRoot));
//File Count
fileCount = fileCount + filesLength;
folderIndex++;
}
//CASE 4: Only folders
else if(filesLength == 0 && subDirsLength > 0)
{
//Add folders of current directory
syncfolderList->AddRange(static_cast<ICollection*>
(subDirs->SyncRoot));
//Folder Count
folderCount = folderCount + subDirsLength;
folderIndex++;
}
//CASE 5: Both Files and Folders
else if(filesLength > 0 && subDirsLength > 0)
{
//Add files
syncfileList->AddRange(static_cast<ICollection*>
(files->SyncRoot));
//Add folders of current directory
syncfolderList->AddRange(static_cast<ICollection*>
(subDirs->SyncRoot));
//File and Folder Count
fileCount = fileCount + filesLength;
folderCount = folderCount + subDirsLength;
folderIndex++;
}
}
delete [] files;
delete [] subDirs;
//If there are more folders in syncfolderList that
// haven't been checked, move to the next folder in the
// list
if(syncfolderList->Count-1 >= loopIndex)
{
currentItem = static_cast<String*>
(syncfolderList->get_Item(loopIndex));
loopIndex++;
folderIndex = 0;
}
else
{
//When all folders in syncfolderList have been
// checked
loopComplete = true;
}
}
//CASE 2: currentItem is a file
else if(File::Exists(currentItem))//filInfo->Exists
{
//Add file to syncfileList
syncfileList->Add(currentItem);
fileCount++;
loopComplete = true;
}
//CASE 3: there was an error
else
{
MessageBox::Show("There was an error loading your FileList!",
"Files Not Added!", MessageBoxButtons::OK,
MessageBoxIcon::Warning);
syncfileList->Clear();
syncfolderList->Clear();
fileCount = 0;
folderCount = 0;
droppedfoldersIndex = folderarrayLength;
loopComplete = true;
hasErrored = true;
}
}
catch(System::Exception* ex)
{
MessageBox::Show(ex->Message, "Warning!",
MessageBoxButtons::OK,
MessageBoxIcon::Warning);
if(MessageBox::Show("Would you like to continue?","Continue?",
MessageBoxButtons::YesNo,
MessageBoxIcon::Question) == DialogResult::Yes)
{
currentItem = static_cast<String*>(syncfolderList->get_Item
(loopIndex));
loopIndex++;
folderIndex = 0;
}
else
{
loopComplete = true;
hasErrored = true;
droppedfoldersIndex = directoryData->Length;
}
}
//UpdateUI
ShowProgress();
}
droppedfoldersIndex++;
}
syncfileList->TrimToSize();
syncfolderList->TrimToSize();
//Unlock the ArrayLists when done
Monitor::Exit(syncfileList);
Monitor::Exit(syncfolderList);
complete = true;
//Return results to listbox in GUI
Display(directoryData);
}
又一个出现的问题是如何在处理大量文件/文件夹时克服不可用的 GUI。这是一个问题,因为该项目需要足够灵活,以便轻量级用户和重度用户都能在不减慢速度的情况下进行备份。经过大量研究,我的解决方案是异步调用 void GetFiles(String* directoryData[], bool subdirsFlag)
,以保持 GUI 的响应性。
我决定使用微软的异步编程方法,如下所示:here
- 定义一个具有与要调用的方法相同签名的委托
- 公共语言运行时会自动为此委托定义
BeginInvoke
和EndInvoke
方法,并具有适当的签名。
- 公共语言运行时会自动为此委托定义
BeginInvoke
方法用于启动异步调用。- 它具有与要异步执行的方法相同的参数,再加上您刚刚创建的委托的一个实例。
BeginInvoke
立即返回,不等待异步调用完成。EndInvoke
方法用于检索异步调用的结果。
EndInvoke
。注意 2:有关我如何异步实现 GetFiles
的详细信息,请参见下面的“使用代码”部分。
使用代码
2Backup 的半完成是一个成就,尽管我的整体方法存在一些问题:
- Windows XP Professional 已提供备份程序,为什么还要重新发明轮子?
- 2Backup 的计划备份支持有限;是否需要高度用户交互?
- 代码不遵循面向对象的прием;代码难以阅读,难以在类似项目中重用。
- 代码量减少
- 代码更易于阅读
- 代码更易于使用
这是类的蓝图:
public __gc class DirectoryList : public Control
DirectoryList
的公共属性和函数如下所示:
- 公共属性
FileCount
- 返回文件数FolderCount
- 返回文件夹数Items
- 返回对 DirectoryList 内部 ListBox ObjectCollection 的引用ShowProgressBar
设置或返回一个布尔标志,用于显示或隐藏 DirectoryList 内部 Listbox 的 progressPanel
- 公共函数
void Build(String* directoryData[], bool subdirsFlag)
- 入口点;将添加到 DirectoryList 中的任何文件/文件夹构建到其中void Copy(String* destinationPath,bool overwrite,bool cdBackup, bool consolidate, bool directorCopy)
- 开始文件复制void Deserialize(String* filename)
- 读取一个二进制文件,其中包含所有已保存的文件和文件夹信息void Remove()
- 从 DirectoryList 中移除选定的项目void Serialize(String* filename)
- 创建一个包含所有文件和文件夹的二进制文件void Sort()
- 按最后写入时间对所有文件和文件夹进行排序
在 DirectoryList
中进行任何数据操作的起点是 Build 函数。
void DirectoryList::Build(String* directoryData[], bool subdirsFlag)
{
//Disable listbox
listbox->Enabled = false;
Cursor = Cursors::WaitCursor;
progressPanel->ProgressBar->Value = 0;
progressPanel->ProgressBar->Visible = true;
buildTime = DateTime::Now;
GetFilesDelegate* getFilesDelegate = new GetFilesDelegate(this,GetFiles);
getFilesDelegate->BeginInvoke(directoryData,subdirsFlag,
new AsyncCallback(this,GetFilesCallback),getFilesDelegate);
}
使用上面提到的相同异步方法,Build
函数创建一个名为 getFilesDelegate
的 Delegate
,其参数如下:
this
- 指向DirectoryList
本身的指针GetFiles
- 私有函数void GetFiles(String* directoryData[], bool subdirsFlag)
的地址。
通过使用我们新创建的 getFilesDelegate
和参数调用 BeginInvoke
:
String* directoryData[]
- 一个包含所有文件和文件夹的字符串数组bool subdirsFlag
- 一个布尔标志,用于决定是否在构建中包含子目录void GetFilesCallback(IAsyncResult* ar)
- 一个带有 IAsyncResult 参数的AsyncCallBack
委托;用于调用getFilesDelegate
对GetFiles
的异步调用上的EndInvoke
。
GetFiles
函数可以异步运行,保持 GUI 响应- 可以调用
GetFilesCallback
函数来调用我们 getFilesDelegate 上的EndInvoke
。
这是 GetFiles
完成后的回调代码:
void DirectoryList::GetFilesCallback(IAsyncResult* ar) { //Get Delegate GetFilesDelegate* getFilesDelegate = static_cast<GetFilesDelegate*>(ar->AsyncState); //Always call EndInvoke getFilesDelegate->EndInvoke(ar); }
我知道你在想什么……代码更少!?……更容易阅读!?是的,这只是类中的代码。现在让我们看看您(作为用户)将做什么:
- 要么将编译好的 DirectoryList.dll 添加到您的工具箱中,然后将控件拖放到新的 Windows 项目中。
- 或者手动创建一个新实例。
private: System::Void btnFolder_Click(System::Object * sender, System::EventArgs * e) { //Normally you would declare your DirectoryList Globally like other //controls but so you can see what I'm doing I declare it locally DirectoryList * myList; FolderBrowserDialog* myFolder = new FolderBrowserDialog(); if(myFolder->ShowDialog() == DialogResult::Cancel) { myFolder->SelectedPath = ""; } else { String* data[] = { myFolder->SelectedPath }; myList->Build(data,cboxSubdirs->Checked); } myFolder->Dispose(); }看看面向对象方法的优美之处!您所需要做的就是调用 Build 并为其提供所需的参数:所有文件和文件夹的字符串数据,以及一个指示是否构建子目录的布尔值。
这种方法在 Copy
和 Sort
函数中也得到了类似的重现。
private: System::Void btnCopy_Click_1(System::Object * sender, System::EventArgs * e) { //Normally you would declare your DirectoryList Globally like other //controls but so you can see what I'm doing I declare it locally DirectoryList * myList; myList->Copy(textBox1->Text,cboxOverwrite->Checked, rbtnCDBackup->Checked, rbtnConsolidateFiles->Checked, rbtnDirectoryCopy->Checked); } private: System::Void btnSort_Click(System::Object * sender, System::EventArgs * e) { //Normally you would declare your DirectoryList Globally like other //controls but so you can see what I'm doing I declare it locally DirectoryList * myList; myList->Sort(); }
关注点
正如 Microsoft 所写,Microsoft,在处理多线程时,返回 DirectoryList 的 Build 结果的唯一方法是通过跨线程调用——也就是说,通过调用 Invoke 或 BeginInvoke 将 GetFiles 函数封送回您的 DirectoryList
的创建线程。这通过私有函数 void Display(String* allData[])
如下实现:
void DirectoryList::Display(String* allData[]) { if(listbox->InvokeRequired == true) { Object* pList[] = { allData }; DisplayDelegate* displayDelegate = new DisplayDelegate(this,Display); //Note: Because you are passing immutable objects into this invoke //method, you do not have to wait for it to finish by calling EndInvoke this->BeginInvoke(displayDelegate, pList); } else { if(hasErrored == true) { Cursor = Cursors::Default; progressPanel->ProgressBar->Visible = false; hasErrored = false; } else if(copy == true || remove == true) { copy = false; remove = false; listbox->Items->Clear(); listbox->Items->AddRange(allData); Cursor = Cursors::Default; progressPanel->ProgressBar->Visible = false; } else { listbox->Items->AddRange(allData); Cursor = Cursors::Default; progressPanel->ProgressBar->Visible = false; } listbox->Enabled = true; filesPanel->Text = String::Concat("Total Files: ", Convert::ToString(fileCount)); foldersPanel->Text = String::Concat("Total Folders: ", Convert::ToString(folderCount)); } }
首先,我创建一个 DisplayDelegate
的实例,其参数如下:
this
- 指向DirectoryList
本身的指针GetFiles
- 私有函数void Display(String* allData[])
的地址。
displayDelegate
和参数调用 BeginInvoke:displayDelegate
- 一个DisplayDelegate
的实例Object* pList[]
- 一个包含所有数据的参数列表
Display
函数将Build
结果返回到内部ListBox
。- 以线程安全的方式完成。
另一个值得关注的点是 void ShowProgress()
函数。ShowProgress
使用 StatusBarProgressPanel 直观地更新 GUI,并具有与上述类似的模式。
void DirectoryList::ShowProgress() { if(InvokeRequired) { //Note: Because you are passing immutable objects into this invoke //method, you do not have to wait for it to finish by calling //EndInvoke IAsyncResult* ar = this->BeginInvoke(showProgressDelegate); } else { if(copy == false || remove == true) { if (progressPanel->ProgressBar->Value == progressPanel->ProgressBar->Maximum) { progressPanel->ProgressBar->Value = 0; progressPanel->ProgressBar->Maximum = 500; //3000 } } else { if (progressPanel->ProgressBar->Value == progressPanel->ProgressBar->Maximum) { progressPanel->ProgressBar->Visible = false; } } progressPanel->ProgressBar->PerformStep(); } }我将一个 StatusBarProgressPanel 添加到一个内部 StatusBar 中,以便跟踪进度并让用户了解文件和文件夹的数量。通过
void InitializeControls(void)
函数,我将 StatusBarProgressPanel 和其他面板添加如下:void DirectoryList::InitializeControls(void) { listbox = new System::Windows::Forms::ListBox(); listbox->Dock = DockStyle::Fill; listbox->HorizontalScrollbar = true; listbox->SelectionMode = SelectionMode::MultiExtended; listbox->AllowDrop = false; statusbar = new StatusBar(); statusbar->Dock = DockStyle::Bottom; statusbar->ShowPanels = true; statusbar->SizingGrip = false; //Namespace.ResourceFiles resources = new System::Resources::ResourceManager("DirectoryList.ResourceFiles", GetType()->Assembly); filesPanel = new StatusBarPanel(); filesPanel->AutoSize = System::Windows::Forms::StatusBarPanelAutoSize::Contents; filesPanel->Text = S"Files : 0"; filesPanel->Icon = static_cast(resources->GetObject("documents.ico")); foldersPanel = new StatusBarPanel(); foldersPanel->AutoSize = System::Windows::Forms::StatusBarPanelAutoSize::Contents; foldersPanel->Width = 110; foldersPanel->Text = S"Folders : 0"; foldersPanel->Icon = static_cast )); progressPanel = new MarkHarmon::Controls::StatusBarProgressPanel(); statusbar->DrawItem += new StatusBarDrawItemEventHandler( this->progressPanel, &StatusBarProgressPanel::ParentDrawItemHandler); progressPanel->AutoSize = System::Windows::Forms::StatusBarPanelAutoSize::Spring; progressPanel->ProgressBar->Maximum = 0; progressPanel->ProgressBar->Value = 0; progressPanel->ProgressBar->Step = 1; progressPanel->ProgressBar->Visible = false; StatusBarPanel* panels[] = { filesPanel, foldersPanel, progressPanel }; statusbar->Panels->AddRange(panels); Control* temp[] = {listbox,statusbar}; Controls->AddRange(temp); }(resources->GetObject("folder.ico"
请注意 Microsoft 的 ResourceManager
类的使用。这是我将文件和文件夹的两个图标嵌入到 StatusBar
中的方法。我能够嵌入图标的唯一方法是使用我在网上找到的一个程序 Resourcer,Resourcer。您只需添加要嵌入的图标,然后将其保存为 ResX 文件。然后,使用 Microsoft 的 ResourceManager
类的一个实例,调用 GetObject
函数,并使用完整的图标文件名作为其参数。
另一个值得关注的点是私有函数 void SortFiles()
。
void DirectoryList::SortFiles()
{
int count = fileCount;
FileInfo* files[] = new FileInfo*[count];
int index = 0;
while(index < count)
{
files[index] = new FileInfo(static_cast<string*>
(syncfileList->get_Item(index)));
index++;
}
//Sort files by last write time
Array::Sort(files,(new CompareFileInfo()));
Monitor::Enter(syncfileList);
syncfileList->RemoveRange(0,count);
index = 0;
while(index < count)
{
syncfileList->Add(files[index]->FullName);
index++;
}
Monitor::Exit(syncfileList);
delete [] files;
}
正如 here 所见,我使用了一个名为 CompareFileInfo
的内部类,它继承自 Microsoft 的 IComparer
接口。
__gc class CompareFileInfo : public IComparer { public: int Compare(Object* x, Object* y) { FileInfo* file = static_cast<fileinfo* />(x); FileInfo* file2 = static_cast<fileinfo* />(y); return DateTime::Compare(file->LastWriteTime, file2->LastWriteTime); } };
它只是从 DirectoryList
的内部 ArrayList
中的所有文件创建了一个 FileInfo
实例的数组,并使用以下语法对其进行排序:
Array::Sort(files,(new CompareFileInfo()));
我最后一个值得关注的点是拖放支持。虽然与 DirectoryList
类没有直接关系,但这是我为 DirectoryList
Demo 添加拖放支持的方法。首先,我创建一个 DragEnter 事件,并将 effect 属性设置为 FileDrop
。
private: System::Void myList_DragEnter(System::Object * sender,
System::Windows::Forms::DragEventArgs * e)
{
//Enable Drag and Drop support
if (e->Data->GetDataPresent(DataFormats::FileDrop))
{
e->Effect = DragDropEffects::Copy;
}
}
然后,我只需创建一个来自 GetData
函数的数组,并将其传递给 Build 函数。我还添加了对 dropped fileList 的支持。
private: System::Void myList_DragDrop(System::Object * sender,
System::Windows::Forms::DragEventArgs * e)
{
String* fileDropArray[];
fileDropArray = static_cast<string*[]>
(e->Data->GetData(DataFormats::FileDrop));
if(fileDropArray[0]->IndexOf(".lst") > 0)
{
myList->Deserialize(fileDropArray[0]);
}
else
{
myList->Build(fileDropArray,cboxSubdirs->Checked);
}
}
结论
有时,在寻找答案的过程中,一个人只会找到更多问题。我在 .NET 框架中的旅程既有趣又激动人心,让我能够探索操作数据时出现的许多不同角度。正如我希望您能看到的,对于任何能够掌握可用工具的人来说,使用 .NET 框架来满足您的数据操作需求是一种真正的享受。以面向对象的方式进行编码使我能够简化代码,最终更容易集成到用户控件中。DirectoryList 是一个用户控件,它允许用户在一个易于使用的包中根据自己的喜好操作数据。我唯一的希望是大家都能从这里的工作中学习并受益。还有很多东西需要学习和改进,包括修复错误和添加新功能,所以我将分享我对未来的计划:
- 完成 .NET 2.0 的更新的 C++/CLI 版本(我快完成了!)。
- 创建一个 C# 版本。
- 修复发现的任何错误;我敢肯定有不少。
来源
- http://msdn.microsoft.com/msdnmag/issues/03/02/Multithreading/default.aspx
- http://msdn.microsoft.com/msdnmag/issues/01/08/Async/
- http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstechart/html/datastructures_guide.asp
- http://www.yoda.arachsys.com/csharp/threads/
- http://www.devx.com/dotnet/Article/16099/0
- http://www.informit.com/guides/content.asp?g=dotnet&seqNum=125&rl=1
- https://codeproject.org.cn/dotnet/embeddedresources.asp
- https://codeproject.org.cn/cs/algorithms/Beginners_Sort.asp
- https://codeproject.org.cn/csharp/AsyncMethodInvocation.asp