Search code examples
c++winapicomwindows-shell

Delete a directory and all of its files using Windows-Shell


I'm writing a MFC program that can occasionally remove a directory with all of its content. These directories will be located in %LOCALAPPDATA%\MyApp so there will be no permission issue (at least I hope).

My first guess was to use CFileFind recursively along with DeleteFile and DeleteDirectory Win32 functions, but a fast look at RemoveDirectory documentation pointed me to SHFileOperation and finally to IFileOperation which, if I have understood, should be the way how a pure Windows Desktop Application has to interact with the file system, so I would exclude the std::filesystem::remove_all solution if possible.

Now, this is surely a problem of mine because I hate COM, but I found this documentation not very clear if not cryptic, so I decided to have a look at the developer's guide where I got to the File Operations Samples which surely helps to understand, but not completely because in my case I have to find the files to delete, so I finally got to this guide which should contains all the informations I'm looking for.

Now, if I got the situation I have to:

  • access the desktop, which is the root of FS;
  • get the path to my directory from the desktop;
  • perform my operations;

In plain ol' C++:

void CWindowsUtilities::RemoveDirectory(HWND parent, CString& directory)
{
    IShellFolder* desktop;
    HRESULT res = SHGetDesktopFolder(&desktop);
    if (SUCCEEDED(res))
    {
        ULONG eaten = 0UL;
        LPITEMIDLIST itemIdList;
        res = desktop->ParseDisplayName(parent, nullptr, directory.GetBuffer(), &eaten, &itemIdList, nullptr);
        if (SUCCEEDED(res))
        {
            IShellFolder* directoryToDelete;
            res = desktop->BindToObject(itemIdList, nullptr, IID_IShellFolder, reinterpret_cast<LPVOID*>(&directoryToDelete));
            if (SUCCEEDED(res))
            {
                IEnumIDList* elementsToDelete;
                res = directoryToDelete->EnumObjects(parent, SHCONTF_INCLUDEHIDDEN | SHCONTF_CHECKING_FOR_CHILDREN | SHCONTF_FOLDERS | SHCONTF_NAVIGATION_ENUM, &elementsToDelete);
                if (res == S_OK)
                {
                    IFileOperation* fileOp;
                    res = CoCreateInstance(__uuidof(FileOperation), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&fileOp));
                    if (SUCCEEDED(res))
                    {
                        res = fileOp->SetOperationFlags(FOFX_ADDUNDORECORD | FOFX_RECYCLEONDELETE);
                        if (SUCCEEDED(res))
                        {
                           res = fileOp->QueryInterface(IID_PPV_ARGS(&fileOp));
                           if (SUCCEEDED(res))
                           {
                             res = fileOp->DeleteItems(elementsToDelete);
                             if (SUCCEEDED(res))
                             {
                                res = fileOp->PerformOperations();
                             }
                           }
                        }
                    }
                    fileOp->Release();
                }
                directoryToDelete->Release();
            }
            CoTaskMemFree(itemIdList);
        }
        desktop->Release();
    }
}

Letting run the code it goes without exceptions, but on DeleteItems I got:

E_NOINTERFACE No such interface supported.

Trying to understand where my problem can be, I've tried to enumerate the files in the directory using:

void CWindowsUtilities::Navigate(IShellFolder* accesso)
{
    IEnumIDListPtr file;
    HRESULT res = accesso->EnumObjects(nullptr, SHCONTF_FOLDERS, &file);
    if (res == S_OK)
    {
        LPITEMIDLIST itemID;
        ULONG fetched;
        while (file->Next(1UL, &itemID, &fetched) == S_OK)
        {
            STRRET nome = { 0 };
            accesso->GetDisplayNameOf(itemID, SHGDN_NORMAL, &nome);
            CoTaskMemFree(itemID);
        }
        file->Release();
     }
}

and so I can see that I can navigate the desktop but not the directory I want to delete. I also tried using SHParseDisplayName instead of desktop->ParseDisplayName but with no success, even if I got a different itemIdList.

What am I doing wrong?


Solution

  • You can use SHFileOperation It has been replaced in Windows vista but still works great.

    SHFILEOPSTRUCT SHFileOp, SHDirOp;
    ZeroMemory(&SHDirOp, sizeof(SHFILEOPSTRUCT));
    SHDirOp.hwnd = NULL;
    SHDirOp.wFunc = FO_DELETE;
    SHDirOp.pFrom = _T("path to folder");
    SHDirOp.pTo = NULL;
    SHDirOp.fFlags =
        FOF_MULTIDESTFILES /* | FOF_NOCONFIRMMKDIR | FOF_NOCONFIRMATION */;
    
    //The Copying Function
    SHFileOperation(&SHDirOp);
    

    If you really want to use the IFileOperator here is how you could use it:

    BOOL deleteFileOrFolder(LPCWSTR fileOrFolderPath) {
        HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
        if (FAILED(hr)) {
            //Couldn't initialize COM library - clean up and return
            MessageBox(NULL, L"Couldn't initialize COM library", L"Whoops", MB_OK | MB_ICONERROR);
            CoUninitialize();
            return FALSE;
        }
        //Initialize the file operation
        IFileOperation* fileOperation;
        hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&fileOperation));
        if (FAILED(hr)) {
            //Couldn't CoCreateInstance - clean up and return
            MessageBox(NULL, L"Couldn't CoCreateInstance", L"Whoops", MB_OK | MB_ICONERROR);
            CoUninitialize();
            return FALSE;
        }
        hr = fileOperation->SetOperationFlags(FOF_NOCONFIRMATION | FOF_SILENT | FOF_NOERRORUI);
        if (FAILED(hr)) {
            //Couldn't add flags - clean up and return
            MessageBox(NULL, L"Couldn't add flags", L"Whoops", MB_OK | MB_ICONERROR);
            fileOperation->Release();
            CoUninitialize();
            return FALSE;
        }
        IShellItem* fileOrFolderItem = NULL;
        hr = SHCreateItemFromParsingName(fileOrFolderPath, NULL, IID_PPV_ARGS(&fileOrFolderItem));
        if (FAILED(hr)) {
            //Couldn't get file into an item - clean up and return (maybe the file doesn't exist?)
            MessageBox(NULL, L"Couldn't get file into an item", L"Whoops", MB_OK | MB_ICONERROR);
            fileOrFolderItem->Release();
            fileOperation->Release();
            CoUninitialize();
            return FALSE;
        }
        hr = fileOperation->DeleteItem(fileOrFolderItem, NULL); //The second parameter is if you want to keep track of progress
        fileOrFolderItem->Release();
        if (FAILED(hr)) {
            //Failed to mark file/folder item for deletion - clean up and return
            MessageBox(NULL, L"Failed to mark file/folder item for deletion", L"Whoops", MB_OK | MB_ICONERROR);
            fileOperation->Release();
            CoUninitialize();
            return FALSE;
        }
        hr = fileOperation->PerformOperations();
        fileOperation->Release();
        CoUninitialize();
        if (FAILED(hr)) {
            //failed to carry out delete - return
            MessageBox(NULL, L"failed to carry out delete", L"Whoops", MB_OK | MB_ICONERROR);
            return FALSE;
        }
        return TRUE;
    }
    

    Both methods had already been resolved in SO in the following links respectively:

    https://stackoverflow.com/a/22226730/1856251

    https://stackoverflow.com/a/68736092/1856251

    Looking at your code I can see that one of the things you are doing wrong is that as per IFileOperation::DeleteItems documentation it takes a pointer of one of the following: IShellItemArray, IDataObject, IEnumShellItems or IPersistIDList. But you are passing a IEnumIDList pointer.