Click here to Skip to main content
Click here to Skip to main content

DirectoryList 2.0

, 18 Aug 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A custom listbox control to help visually manipulate data.

new1.png

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.

charset.png

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 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

nirvansk815
Other
United States United States
No Biography provided

Comments and Discussions

 
NewsUpdated Pinmembernirvansk8152-Apr-08 1:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150129.1 | Last Updated 19 Aug 2008
Article Copyright 2008 by nirvansk815
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid