65.9K
CodeProject 正在变化。 阅读更多。
Home

DirectoryList

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (2投票s)

2007年1月31日

CPOL

10分钟阅读

viewsIcon

34972

downloadIcon

1015

一个自定义列表框控件,用于帮助操作数据

Sample image

引言

每次使用计算机时,其数据都会为许多独特而有趣的任务进行操作:播放MP3、浏览图片,甚至阅读电子邮件……在所有情况下,这些数据都可以移动、读取、写入、复制等,到存储子系统内的任意目标。令人惊讶的是,所有这一切都在幕后进行,让普通用户享受当今计算机所提供的便利。然而,总会有一个时刻,某些数据不仅对计算机很重要,对用户也很重要。无论一个人的数据是否重要,最终都是主观的,但是如何操作这些数据的问题,是我探索.NET框架如何提供帮助的最初动机。自从我第一次涉足这个项目以来,这条道路漫长而激动人心,这不仅是因为已经提供了许多解决方案,还因为这个问题的广度和复杂性。我写这篇文章的目的是分享我最初遇到的问题、想法和解决方案,以帮助其他人更好地理解.NET框架中操作数据的动态,特别是我是如何利用它来满足我的需求的。

背景

虽然操作数据可以有很多含义,但我最初的问题是创建一个程序,可以轻松地将文件和文件夹备份到用户指定的指定位置。这个项目我称之为 2Backup,它包含一个用于所有添加的文件/文件夹的 ListBox,用于将文件/文件夹添加到 ListBox 和从中移除的按钮,一个目标文本框,以及一个开始备份的复制按钮。这是基本布局的图片。

Sample image


这个想法是,通过拖放,或者使用添加按钮,将您想要备份的文件或文件夹添加到 ListBox 中。选择目标位置后,选择一个复制模式,然后开始备份。我选择了三种复制模式:
  • 目录复制 - 复制所有文件/文件夹,保留源文件夹结构
  • 合并文件 - 将所有文件/文件夹复制到一个文件夹中
  • CD备份 - 与目录复制相同,但每 700MB 创建一个新文件夹

遇到的第一个问题是如何使用 .NET 框架实现一个将文件和文件夹添加到列表框的函数。我决定使用 OpenFileDialog,当 `multiSelect` 设置为 `true` 时,它会将所有选定的文件复制到列表框中。这种方法存在一些问题:

  • .NET OpenFileDialog 类在出现错误之前只允许选择有限数量的文件
  • 加载文件夹比选择大量文件更容易

Sample image

注意:如果有人能提供“选择文件过多”错误的解决方法,请随时与我联系。

这两个问题的解决方案是使用 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

  • 定义一个具有与要调用的方法相同签名的委托
    • 公共语言运行时会自动为此委托定义 BeginInvokeEndInvoke 方法,并具有适当的签名。
  • BeginInvoke 方法用于启动异步调用。
    • 它具有与要异步执行的方法相同的参数,再加上您刚刚创建的委托的一个实例。
  • BeginInvoke 立即返回,不等待异步调用完成。
  • EndInvoke 方法用于检索异步调用的结果。
注意 1:在异步调用完成后,务必调用 EndInvoke

注意 2:有关我如何异步实现 GetFiles 的详细信息,请参见下面的“使用代码”部分。

使用代码

2Backup 的半完成是一个成就,尽管我的整体方法存在一些问题:

  • Windows XP Professional 已提供备份程序,为什么还要重新发明轮子?
  • 2Backup 的计划备份支持有限;是否需要高度用户交互?
  • 代码不遵循面向对象的прием;代码难以阅读,难以在类似项目中重用。
我的解决方案是使用许多相同的功能重写代码,只是以更面向对象的方式实现,从而尽可能轻松地集成到用户控件中。虽然我将提供我的 2Backup 代码作为参考,但本文其余部分将重点介绍我改进后的用户控件代码,该控件封装了相同的功能。在 2Backup 完成后,我想做的第一件事是创建一个继承自 Microsoft 的 Control 类的托管 C++ 类。这次我的目标是吸收 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 函数创建一个名为 getFilesDelegateDelegate,其参数如下:

  • this - 指向 DirectoryList 本身的指针
  • GetFiles - 私有函数 void GetFiles(String* directoryData[], bool subdirsFlag) 的地址。

通过使用我们新创建的 getFilesDelegate 和参数调用 BeginInvoke

  • String* directoryData[] - 一个包含所有文件和文件夹的字符串数组
  • bool subdirsFlag - 一个布尔标志,用于决定是否在构建中包含子目录
  • void GetFilesCallback(IAsyncResult* ar) - 一个带有 IAsyncResult 参数的 AsyncCallBack 委托;用于调用 getFilesDelegateGetFiles 的异步调用上的 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 并为其提供所需的参数:所有文件和文件夹的字符串数据,以及一个指示是否构建子目录的布尔值。

这种方法在 CopySort 函数中也得到了类似的重现。

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(resources->GetObject("folder.ico"));

    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);

}

请注意 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# 版本。
  • 修复发现的任何错误;我敢肯定有不少。
感谢大家阅读我在 The Code Project 发表的第一篇文章!

来源

© . All rights reserved.