Introduction
The What
The DirectoryList
control is a custom listbox control that can be used:
- To keep track of the number of files and folders in a directory (drag and drop support).
- To copy files from one folder to another.
- To consolidate files from many folders into one.
- To create backup folders of 700MB (the size of a CD-ROM).
- To search for certain file types within folders.
Note: This is the second version of the DirectoryList
class. For more detailed info on why this project was started and also on some of the details of the class, please read my first article.
The Why
The DirectoryList
control is most useful in situations where a user needs to visually manipulate or keep track of important data on their computer.
The How
Note: Make sure you have the latest Visual C++ Redistributable Package installed.
Here is the blue print of the class:
public ref class DirectoryList : public Control
DirectoryList
's public properties and functions are listed below:
FileCount
- Returns file count.FolderCount
- Returns folder count.Items
- Returns a reference to DirectoryList
's internal ListBox
, ObjectCollection
.ShowProgressBar
- Sets or returns a boolean flag to show or hide DirectoryList
's internal ListBox
, progressPanel
.TraverseDirectory
- Booelan value allowing DirectoryList
to traverse through sub-directories
void Build(array<string^ />^ data, bool subdirsFlag)
- Starting point; builds files/folders added into the DirectoryList
.void Copy(String^ destinationPath,bool overwrite, bool cdBackup, bool consolidate, bool directorCopy)
- Starts file copy.void Deserialize(String^ filename)
- Reads a binary file with all saved files and folder info.void Remove()
- Removes selected items from the DirectoryList
.void SearchFor(String ^filetype, ListBox^ results)
- Searches for files with a certain file type.void Serialize(String^ filename)
- Creates a binary file of all files and folders.
To add a DirectoryList
to a project from within Visual Studio, right click inside your Toolbox and select "Choose Items". After browsing for DirectoryList.dll, Visual Studio should add the DirectoryList
to your Toolbox so you can add it to your project like any other control. If you should choose to compile the project yourself, there are a few minor quirks.
You need to make sure the project "Character Set" is set to "Multi-Byte". You also need to make sure your project is set to compile to DLL. Also, make sure you add StatusBarProgressControl.dll to your project's references.
Using the Code
Construct a DirectoryList
as follows:
DirectoryList ^myList = gcnew DirectoryList();
this->myList->AllowDrop = true;
this->myList->ContextMenuStrip = this->contextMenuStrip1;
this->myList->Location = System::Drawing::Point(14, 64);
this->myList->Name = L"myList";
this->myList->ShowProgressBar = false;
this->myList->Size = System::Drawing::Size(316, 250);
this->myList->TraverseDirectory = false;
this->myList->DragDrop += gcnew System::Windows::Forms::DragEventHandler(this,
&Form1::myList_DragDrop);
this->myList->DragEnter += gcnew System::Windows::Forms::DragEventHandler(this,
&Form1::myList_DragEnter);
this->Controls->Add(this->myList);
To add all files inside a folder and all subfolders, use the following code:
private: System::Void btnFolder_Click(System::Object^ sender, System::EventArgs^ e)
{
FolderBrowserDialog^ myFolder = gcnew FolderBrowserDialog();
if(myFolder->ShowDialog() == System::Windows::Forms::DialogResult::Cancel)
{
myFolder->SelectedPath = "";
}
else
{
array<string^> ^<string^ />data = gcnew array<string^ />(1);
data[0] = myFolder->SelectedPath;
myList->Build(data,cboxSubdirs->Checked);
delete [] data;
}
delete myFolder;
}
and to copy files from one folder to another:
private: System::Void btnCopy_Click(System::Object^ sender, System::EventArgs^ e)
{
if(textBox1->Text->Length > 0)
{
myList->Copy(textBox1->Text,cboxOverwrite->Checked,rboxCDbackup->Checked,
rbConsolidate->Checked,rbDcopy->Checked);
}
}
Points of Interest
In the light of updating the DirectoryList
class to C++/CLI, I decided to perform a performance analysis with Microsoft's CLR Profiler. It turns out I had some problems within my Build
function. Specifically, the Build
function calls the private member function TraverseFiles
, that in turn calls .NET's Directory::Exists
, Directory::GetFiles
, and Directory::GetDirectories
functions. The problem with these functions is that they allocate too much memory and slow the Build
function down. I decided to write a Win32 API wrapper class to encapsulate faster Win32 API functions and improve Build
's performance. Please see the included Excel spreadsheet for more details, but going from a pure .NET solution to the API wrapper reduced memory allocation by 35MB. It also reduced the relocated bytes by 17MB. In addition to the new wrapper class, I was also able to take advantage of a few of Microsoft's new classes included in .NET 2.0. One such class is System::Collections::Generic::LinkedList
. By moving from an ArrayList
to a LinkedList
, I was able to reduce my TraverseFiles
function's size by 188 lines of code.
Here is the class that speeds up the DirectoryList
:
class DirectoryAPI
{
public:
DirectoryAPI();
~DirectoryAPI();
bool DirectoryExists(char *argv);
char** FindFiles(char *argv);
char** FindDirectories(char *argv);
int numberOfFiles;
int numberOfFolders;
};
For example, here is how I implemented the native FindFiles
function:
char** DirectoryAPI::FindFiles(char* argv)
{
WIN32_FIND_DATA FindFileData;
HANDLE hFind = INVALID_HANDLE_VALUE;
DWORD dwError;
LPTSTR DirSpec;
size_t length_of_arg;
char **result;
DirSpec = (LPTSTR) malloc (BUFSIZE);
if( DirSpec == NULL )
{
goto Cleanup;
}
StringCbLength(argv, BUFSIZE, &length_of_arg);
if (length_of_arg > (BUFSIZE - 2))
{
goto Cleanup;
}
StringCbCopyN (DirSpec, BUFSIZE, argv, length_of_arg+1);
StringCbCatN (DirSpec, BUFSIZE, TEXT("\\*"), 2*sizeof(TCHAR));
numberOfFiles = 0;
hFind = FindFirstFile(DirSpec, &FindFileData);
if (hFind == INVALID_HANDLE_VALUE)
{
}
else
{
while (FindNextFile(hFind, &FindFileData) != 0)
{
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
}
else
{
numberOfFiles++;
}
}
dwError = GetLastError();
FindClose(hFind);
if (dwError != ERROR_NO_MORE_FILES)
{
goto Cleanup;
}
}
result = new char*[numberOfFiles];
if(numberOfFiles > 0)
{
hFind = FindFirstFile(DirSpec, &FindFileData);
if (result == NULL)
{
}
if (hFind == INVALID_HANDLE_VALUE)
{
}
else
{
int myCount = 0;
while (FindNextFile(hFind, &FindFileData) != 0)
{
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
}
else
{
int fileNameLength = strlen(FindFileData.cFileName);
int pathLength = strlen(argv);
int totalLength = fileNameLength + pathLength;
result[myCount] = new char[totalLength + 2];
result[myCount][pathLength] = '\\';
for(int j = pathLength; j--;)
{
result[myCount][j] = argv[j];
}
int p = totalLength;
result[myCount][totalLength+1] = '\0';
for(int k = fileNameLength; k--;)
{
result[myCount][p] = FindFileData.cFileName[k];
p--;
}
myCount++;
}
}
dwError = GetLastError();
FindClose(hFind);
if (dwError != ERROR_NO_MORE_FILES)
{
goto Cleanup;
}
}
}
Cleanup:
free(DirSpec);
return result;
}
Here is the wrapper class that glues everything together:
ref class DirectoryAPIWrapper
{
public:
DirectoryAPIWrapper();
~DirectoryAPIWrapper();
array<string^ />^ GetFiles(String^ currentDirectory);
array<string^ />^ GetDirectories(String^ currentDirectory);
bool DirectoryExists(String^ currentDirectory);
private:
DirectoryAPI* api;
};
Here is how I implemented the GetFiles
function, which in turn calls the native FindFiles
function:
array<String^>^ DirectoryAPIWrapper::GetFiles(System::String ^currentDirectory)
{
array<String^>^ answer;
try
{
char *nativeArg =
(char*)(Marshal::StringToHGlobalAnsi(currentDirectory).ToPointer());
char **result = api->FindFiles(nativeArg);
answer = gcnew array<string^ />(api->numberOfFiles);
if(api->numberOfFiles > 0)
{
for(int i = api->numberOfFiles; i--;)
{
answer[i] = gcnew String(result[i]);
delete [] result[i];
result[i] = nullptr;
}
}
if(result != nullptr)
{
delete [] result;
result = nullptr;
}
Marshal::FreeHGlobal(IntPtr(nativeArg));
}
catch(System::Exception ^ex)
{
MessageBox::Show(ex->Message,"GetFiles Error");
}
return answer;
}
And for completeness, here is my final updated TraverseFiles
function:
void DirectoryList::TraverseFiles(void)
{
DirectoryAPIWrapper^ myWrapper = gcnew DirectoryAPIWrapper();
beginFileCount = fileCount;
try
{
for(int i=0; i < directoryData->Length; i++)
{
if(myWrapper->DirectoryExists(directoryData[i]))
{
llfolders->AddLast(directoryData[i]);
folderCount++;
Generic::LinkedListNode<string^ /> ^currentNode = llfolders->Last;
while(currentNode != nullptr)
{
array<String^>^ files = myWrapper->GetFiles(currentNode->Value);
if(files->Length > 0)
{
interior_ptr<string^ /> p = &files[0];
while(p != &files[0] + files->Length)
{
llfiles->AddLast(*p++);
fileCount++;
}
}
if(traverseDirectory)
{
array<String^>^ subDirs =
myWrapper->GetDirectories(currentNode->Value);
if(subDirs->Length > 0)
{
interior_ptr<string^ /> p = &subDirs[0];
while(p != &subDirs[0] + subDirs->Length)
{
llfolders->AddLast(*p++);
folderCount++;
}
}
}
currentNode = currentNode->Next;
ShowProgress();
}
}
else
{
llfiles->AddLast(directoryData[i]);
fileCount++;
}
}
}
catch(System::Exception^ ex)
{
MessageBox::Show(ex->Message);
}
delete myWrapper;
Display();
}
Thanks to Dandy Cheung and some research, I added the ability to search for file types within the DirectoryList
. (Try the updated demo app.) Here is the function that SearchFor
calls to find the files:
void DirectoryList::GetResults(Object ^stateInfo)
{
try
{
SearchInfo ^mySearch = dynamic_cast<searchinfo^ />(stateInfo);
if(fileCount > 0)
{
Generic::LinkedListNodearray<String^>^ currentNode = llfolders->First;
String ^filetypeCopy = mySearch->filetype;
while(currentNode != nullptr)
{
mySearch->filetype = currentNode->Value + filetypeCopy;
mySearch->currentSearchNode = currentNode;
array<object^>^ myArray = gcnew array<object^ />(1);
myArray[0] = mySearch;
mySearch->results->Invoke(gcnew
ReturnResultsDelegate(this,&DirectoryList::ReturnResults),myArray);
currentNode = currentNode->Next;
}
}
}
catch(System::Exception^ ex)
{
MessageBox::Show(ex->Message);
}
}
And here is the code to return the results:
void DirectoryList::ReturnResults(Object^stateInfo)
{
SearchInfo ^mySearch = dynamic_cast<SearchInfo^>(stateInfo);
char *nativeArg =
(char*)(Marshal::StringToHGlobalAnsi(mySearch->filetype).ToPointer());
::SendMessage((HWND)mySearch->results->Handle.ToPointer(),
LB_DIR,DDL_DIRECTORY,(LPARAM)nativeArg);
if(mySearch->currentSearchNode->Next == nullptr)
{
mySearch->results->Cursor = Cursors::Default;
MessageBox::Show(::SendMessage((HWND)mySearch->results->Handle.ToPointer(),
LB_GETCOUNT,0,0).ToString() + " files found!",
"Results",MessageBoxButtons::OK,MessageBoxIcon::Information);
mySearch->results->EndUpdate();
}
}
History
- 8-18-08: Minor typos and clarifications in article. Renamed a few of the private member functions for clarity. Fixed a bug in the demo where if you dragged and dropped a .lst file, it wouldn't build the subdirectories. The progress bar now shows the
DirectoryList
building its files/folders. This can be improved, but it's better than nothing. - 5-20-08:
GetResults()
now runs asynchronously, and also simplified the callback code using MethodInvokers
. - 4-2-08: Minor bug fixes, redesigned demo application to include support for file type search.
- Version 2.0: Added the Win32 API wrapper, and also took away the sort and verify functions to simplify the class. Rebuilt the class for C++/CLI.