DirectoryList 2.0






3.43/5 (12 votes)
A custom listbox control to help visually manipulate data.
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 toDirectoryList
's internalListBox
,ObjectCollection
.ShowProgressBar
- Sets or returns a boolean flag to show or hideDirectoryList
's internalListBox
,progressPanel
.TraverseDirectory
- Booelan value allowingDirectoryList
to traverse through sub-directories
void Build(array<string^ />^ data, bool subdirsFlag)
- Starting point; builds files/folders added into theDirectoryList
.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 theDirectoryList
.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);
//Make sure you delete allocated memory
//after using FindFiles or FindDirectories
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 )
{
//Insufficient memory available
//MessageBox(0,"NULL","Welcome Message",1);
goto Cleanup;
}
// Check that the input is not larger than allowed.
StringCbLength(argv, BUFSIZE, &length_of_arg);
if (length_of_arg > (BUFSIZE - 2))
{
//Input directory is too large.
goto Cleanup;
}
// Prepare string for use with FindFile functions. First,
// copy the string to a buffer, then append '\*' to the
// directory name.
StringCbCopyN (DirSpec, BUFSIZE, argv, length_of_arg+1);
StringCbCatN (DirSpec, BUFSIZE, TEXT("\\*"), 2*sizeof(TCHAR));
//Find the number of files in the directory.
numberOfFiles = 0;
hFind = FindFirstFile(DirSpec, &FindFileData);
if (hFind == INVALID_HANDLE_VALUE)
{
//Invalid file handle.
}
else
{
// Get a Count of all the files in the directory.
while (FindNextFile(hFind, &FindFileData) != 0)
{
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
//Skip
}
else
{
numberOfFiles++;
}
}
dwError = GetLastError();
FindClose(hFind);
if (dwError != ERROR_NO_MORE_FILES)
{
//FindNextFile error.
goto Cleanup;
}
}
result = new char*[numberOfFiles];
if(numberOfFiles > 0)
{
hFind = FindFirstFile(DirSpec, &FindFileData);
if (result == NULL)
{
//Not enough memory
}
if (hFind == INVALID_HANDLE_VALUE)
{
//Invalid file handle.
}
else
{
// List all the files in the directory.
int myCount = 0;
while (FindNextFile(hFind, &FindFileData) != 0)
{
if(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
//Skip
}
else
{
int fileNameLength = strlen(FindFileData.cFileName);
int pathLength = strlen(argv);
int totalLength = fileNameLength + pathLength;
result[myCount] = new char[totalLength + 2];
//Copy file path
result[myCount][pathLength] = '\\';
for(int j = pathLength; j--;)
{
result[myCount][j] = argv[j];
}
//Concat file name to file path
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)
{
//FindNextFile error.
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)
{
//Get interior pointer to array
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)
{
//Get interior pointer to array
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;
//Return results to listbox in GUI
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)
{
//"C:\\WINDOWS\\*.*";
mySearch->filetype = currentNode->Value + filetypeCopy;
mySearch->currentSearchNode = currentNode;
//Parameter Array
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());
//To solve your problem you have to link your project with the User32.lib file.
//If you're using the IDE to compile go to the project
//properties->linker->input->additional dependencies
//and add User32.lib
::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 usingMethodInvokers
. - 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.