31 июл. 2012 г.

Работа с корзиной

Эту статью я решил написать после того, как почти ничего не нашел в Интернете касательно рассматриваемого вопроса. В основном мне попадались вопросы "Как удалить файл в корзину" и "Как восстановить файл из корзины". Здесь мы рассмотрим как эти вопросы, так и другие:

Как получить список файлов, содержащихся в корзине;
Как восстановить файл из корзины;
Как удалить файл из корзины (т. е. окончательно);
Как показать окно "Свойства" для элемента корзины;
Как очистить корзину.




Здесь лучше всего создать приложение Windows Forms и работать с ним.
Создайте приложение Windows Forms с произвольным названием. После загрузки проекта в открывшемся дизайнере разместите на форме следующие компоненты:


  • ToolStripContainer;
  • ListView в центральной панели ToolStripContainer; назовите его lvRecycle, поставьте свойство Dock равным Fill, свойство View равным Details и добавьте колонки:
  • Имя;
  • Первоначальное расположение;
  • Тип;
  • Размер;
  • Дата создания.
  • ToolStrip в верхней панели ToolStripContainer; разместите на нем следующие кнопки (в скобках -- имя кнопки):
  • Обновить (tsbRefresh);
  • Разделитель;
  • Восстановить (tsbRestore);
  • Удалить безвозвратно (tsbDelete);
  • Свойства (tsbProperties);
  • Разделитель;
  • Очистить корзину (tsbClearRecycle).

    В результате у Вас должна быть на экране примерно такая картинка (значки кнопкам можете поставить свои):

    Теперь перейдем к коду. Вы, наверно, не имеете никакого представления о том, что делать дальше. Я сначала тоже не имел. Однако в ходе почти безрезультатных поисков я выяснил, что для работы с корзиной можно воспользоваться библиотекой shell32.dll, которая является одной из базовых библиотек в Windows. Эту библиотеку можно использовать в Visual Studio, для этого ее достаточно добавить в список "References" (Ссылки). Щелкните правой кнопкой мыши по имени проекта в "Обозревателе проектов" и в контекстном меню выберите "Add reference" (Добавить ссылку). В появившемся окне откройте вкладку Browse (Обзор) и пройдите по пути C:\Windows\System32\ (у Вас может быть другая буква диска). Далее в списке найдите и дважды щелкните для выбора shell32.dll. Библиотека будет добавлена в папку References проекта под именем Shell32.

    Теперь откройте Редактор кода для формы, на которой мы настроили интерфейс (я назвал эту форму frmRecycle) и в самом верху, в списке "using..." добавьте ссылку на Shell32:

    using Shell32;
    
    Сразу после строчки
    public partial class frmRecycle : Form
    
    где frmRecycle -- название формы, вставьте следующий код:

    //Некоторые флаги для API-функции SHEmptyRecycleBin
    //Не отображать диалог с уведомлением об удалении объектов
    const int SHERB_NOCONFIRMATION = 0x00000001;
    //Не отображать диалог с индикатором прогресса
    const int SHERB_NOPROGRESSUI = 0x00000002;
    //Когда операция закончится, не пригрывать звук
    const int SHERB_NOSOUND = 0x00000004;
    
    //API-функция очистки корзины
    [DllImport("shell32.dll")]
    static extern int SHEmptyRecycleBin(IntPtr hWnd, string pszRootPath, uint dwFlags);
    
    //Перечисление и API-функция для открытия файлов. Зачем это нужно -- в самом конце статьи
    public enum ShowCommands : int
    {
       SW_HIDE = 0,
       SW_SHOWNORMAL = 1,
       SW_NORMAL = 1,
       SW_SHOWMINIMIZED = 2,
       SW_SHOWMAXIMIZED = 3,
       SW_MAXIMIZE = 3,
       SW_SHOWNOACTIVATE = 4,
       SW_SHOW = 5,
       SW_MINIMIZE = 6,
       SW_SHOWMINNOACTIVE = 7,
       SW_SHOWNA = 8,
       SW_RESTORE = 9,
       SW_SHOWDEFAULT = 10,
       SW_FORCEMINIMIZE = 11,
       SW_MAX = 11
    }
    [DllImport("shell32.dll")]
    static extern IntPtr ShellExecute(
        IntPtr hwnd,
        string lpOperation,
        string lpFile,
        string lpParameters,
        string lpDirectory,
        ShowCommands nShowCmd);
    
    Shell shell;//экземпляр интерфейса Shell
    

    Получение содержимого корзины

    Создадим метод LoadRecycle(), который будет перебирать содержимое корзины и загружать в lvRecycle. В виде метода мы создаем это для того, чтобы вызывать из обработчика события загрузки формы и из обработчика щелчка по кнопке tsbRefresh.

    /// Загрузка элементов корзины
    
    private void LoadRecycle()
    {
       shell = new Shell();//создаем новый экземпляр интерфейса Shell
    
       lvRecycle.Items.Clear();//очищаем список lvRecycle
    
       ListViewItem lvi;
       //получаем экземпляр интерфейса Folder, уже как бы настроенный на корзину
       Folder recycleBin = shell.NameSpace(10);
    
       //перебираем всё, что содержится в recycleBin, т. е. файлы и папки
       foreach (FolderItem2 f in recycleBin.Items())
       {
       //Добавляем новый элемент в список и присваиваем полученный ListViewItem переменной lvi.
           //Здесь именем элемента является полное имя файла или папки, текстом, отображаемым
           //в колонке "Имя" -- имя файла или папки
           lvi = lvRecycle.Items.Add(f.Path, f.Name, "");
           //добавляем первоначальное расположение файла
           lvi.SubItems.Add(f.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}2"));
           //добавляем тип файла
           lvi.SubItems.Add(f.Type);
           //если элемент -- папка, то размер не вычисляем
           if (f.IsFolder)
           {
              lvi.Tag = "d";//это нужно для последующего отделения папок от файлов
              lvi.SubItems.Add("");
           }
           //иначе показываем размер
           else
           {
              lvi.Tag = "f";
              lvi.SubItems.Add(f.Size.ToString() + " B");//размер будет отображен в байтах
           }
           //дата удаления
           lvi.SubItems.Add(((DateTime)f.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}3")).ToString());
       }
       //В ресурсах проекта хранятся два значка -- пустой корзины и полной.
       //Если в корзине что-то есть, показываем значок полной корзины, иначе -- пустой
       if (lvRecycle.Items.Count > 0)
           this.Icon = LUNAticExplorer.Properties.Resources.recycle_full;
       else
           this.Icon = LUNAticExplorer.Properties.Resources.recycle_empty;
    
       //нужно всегда освобождать ресурсы из-под более ненужного объекта COM
       //(каковым Shell32 и является)
       Marshal.FinalReleaseComObject(shell);
    }

    Разбираемся в писанине. Здесь есть три очень интересных момента.

    Что может являться путем к файлу или папке в корзине? Сначала я решил, что в f.Path содержится первоначальное расположение и имя файла. При использовании строки

    lvi.SubItems.Add(f.Path.Substring(0, f.Path.LastIndexOf("\\") + 1));
    
    получилось это:













    Корзина как папка не существует, на самом деле это несколько папок "$RECYCLE.BIN", которые созданы на всех жестких дисках, которые есть в системе. В каждой такой папке хранятся только те файлы, которые были удалены с того же диска, где находится эта папка. Свойство f.Path возвращает путь, где файл находится сейчас, и имя файла. Для чего это можно использовать, увидим далее.

    Что тогда означает запись "f.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}2")"? На пример, возвращающий первоначальное расположение файла таким способом, я наткнулся в комментариях к одной статье. Дальнейший поиск позволил выяснить, что значит "{9B174B33-40FF-11D2-A27E-00C04FC30871}".

    Если не вдаваться в подробности кодинга на C++, то это некий GUID, являющийся первым членом структуры SHCOLUMNID.
    Второй член этой структуры принимает значение типа DWORD, и если первому члену присвоен приведенный выше GUID, то второму можно присвоить значение 2 (для первоначального расположения) или 3 (для даты удаления). Это навело меня на мысль, что если написать

    lvi.SubItems.Add(((DateTime)f.ExtendedProperty("{9B174B33-40FF-11D2-A27E-00C04FC30871}3")).ToString());
    
    то будет добавлена дата удаления данного файла.
    Итак, с получением содержимого корзины мы разобрались. (В обработчике щелчка по кнопке "Обновить" и обработчике события загрузки формы не забудьте вызывать этот метод.)


    Восстановление файла из корзины

    Дважды щелкните по кнопке "Восстановить", чтобы создать обработчик щелчка по ней. Сначала я приведу код, затем мы его разберем.


    private void tsbRestore_Click(object sender, EventArgs e)
    {
       shell = new Shell();//создаем новый экземпляр интерфейса Shell
       Folder recycler = shell.NameSpace(10);//настраиваемся на корзину :)
       //перебираем все выделенные элементы lvRecycle, поскольку lvRecycle.MultiSelect = true
       foreach (ListViewItem lvi in lvRecycle.SelectedItems)
       {
           //ищем элемент lvRecycle в корзине
           foreach (FolderItem2 fi in recycler.Items())
              if ((fi.Name == lvi.Text) && (fi.Path == lvi.Name))
                 //нашли, теперь перебираем коллекцию т. н. "действий", в которых содержатся, в
                 //частности, пункты контекстного меню Проводника (мне так показалось :))
                 foreach (FolderItemVerb FIVerb in fi.Verbs())
                    if (FIVerb.Name.ToUpper().Contains("estore".ToUpper()))//*
                        FIVerb.DoIt();//выполняем действие
            lvRecycle.SelectedItems[0].Remove();//удалаем элемент из списка
       }
       //уничтожаем экземпляр COM-объекта
       Marshal.FinalReleaseComObject(shell);
    }
    
    Разбираем: здесь только один интересный момент, он отмечен звездочкой.
    Почему я написал "ESTORE", а не "RESTORE"? Произведя последовательную отладку и используя точки останова, я нашел, что в коллекции действий содержится "RESTORE", у которого вторая буква подчеркнута (как бы перед буквой поставили символ "&"). То есть как бы вся строка до этой буквы обрезается.

    Идем дальше.


    Окончательное удаление файла из корзины

    Создайте обработчик щелчка по кнопке "Удалить" двойным щелчком по ней.

    Здесь технология удаления из корзины такая же, как и восстановления. Хочу отметить, что в Интернете нет решений об удалении файла из корзины на C# (но, может, я плохо искал), поэтому до этого способа (как и способа отображения окна "Свойства" для выбранного элемента корзины. о чем ниже) я дошел сам.

    Если для восстановления мы искали действие "ESTORE" и выполняли его, то для удаления нужно найти действие "DELETE". Но трудность заключается в том, что у этого действия символ "&" может находиться в любом месте имени, и. соответственно, нужно правильно его задать. Я сделал это при помощи точки останова, поставленной на строке 15 кода для кнопки "Восстановить". Наводя курсор мыши на FIVerb.Name, я определял, какое действие проверяется сейчас, а затем нажимал кнопку "Продолжить" в среде VS. Таким образом, я выяснил, что вместо "DELETE" нужно писать "DELETE" (подчеркнута первая буква строки). Смотрим получившийся код:

    private void tsbDelete_Click(object sender, EventArgs e)
    {
       //здесь все без комментариев, т к. код такой же, как и для кнопки "Восстановить"
       shell = new Shell();
    
       Folder recycler = shell.NameSpace(10);
       foreach (ListViewItem lvi in lvRecycle.SelectedItems)
       {
          foreach (FolderItem2 fi in recycler.Items())
             if ((fi.Name == lvi.Text) && (fi.Path == lvi.Name))
                foreach (FolderItemVerb FIVerb in fi.Verbs())
                   if (FIVerb.Name.ToUpper().Contains("delete".ToUpper()))
                      FIVerb.DoIt();
            lvRecycle.SelectedItems[0].Remove();
       }
       Marshal.FinalReleaseComObject(shell);
    }
    
    Есть у результата выполнения этого кода только один изъян: для каждого удаляемого элемента Вас спрашивают, действительно ли Вы хотите удалить то, что Вы удаляете. Если было выбрано 4 объекта для удаления, что Вам придется 4 раза нажать кнопку "Да" в ответ на вопрос. Я пытался найти API-функцию наподобие SHFileOperation, которая удаляла бы файлы из корзины без сообщений, но мои поиски не увенчались успехом.

    Отображение окна "Свойства" для элемента корзины
    Это делается так же, как удаление файла из корзины. Описанным методом я установил, что вместо "PROPERTIES" нужно писать "ROPERTIES", поскольку здесь подчеркнута вторая буква.

    Следующий код "повесьте" на кнопку "Свойства".


    private void tsbProperties_Click(object sender, EventArgs e)
    {
       //опять без комментариев...
       shell = new Shell();
       Folder recycler = shell.NameSpace(10);
       foreach (FolderItem2 fi in recycler.Items())
          if ((fi.Name == lvRecycle.SelectedItems[0].Text) && (fi.Path == lvRecycle.SelectedItems[0].Name))
             foreach (FolderItemVerb FIVerb in fi.Verbs())
                if (FIVerb.Name.ToUpper().Contains("roperties".ToUpper()))
                   FIVerb.DoIt();
        Marshal.FinalReleaseComObject(shell);
    }
    

    В результате Вы увидите окно "Свойства", которое отображается в Проводнике для любого удаленного файла или папки.
    Очистка корзины
    Это делается API-функцией SHEmptyRecycleBin следующим образом:

    private void tsbClearRecycle_Click(object sender, EventArgs e)
    {
       SHEmptyRecycleBin(IntPtr.Zero, "", SHERB_NOSOUND);
    }
    
    Объявление функции и флаги мы добавили перед разделом "Получение содержимого корзины". Если в качестве второго параметра указать букву диска, то корзина будет очищена только на этом диске.

    Что еще можно сделать
    Помните, я писал выше, что нам пригодится полное имя файла или папки в корзине? Так вот, с этим полным именем можно делать все то же, что и с обычным файлом. В частности, открыть его для просмотра содержимого, чего, кстати, не хочет делать Проводник.

    Для открытия файла или папки (и много еще для чего) используется функция ShellExecute, ее мы объявили перед разделом "Получение содержимого корзины" вместе с SHEmptyRecycleBin.

    Создайте обработчик события двойного щелчка по lvRecycle. Это событие будет возникать, в частности, при двойном щелчке по элементу в lvRecycle (но и по любой колонке тоже, но мы сейчас не рассматриваем работу с ListView). Следующий код поместите в созданный обработчик:



    if (ShellExecute(IntPtr.Zero, "open", lvRecycle.SelectedItems[0].Name, "", "", ShowCommands.SW_SHOWNORMAL).ToInt32() == 31)
    {
       ShellExecute(IntPtr.Zero, "", "rundll32.exe", "shell32.dll,OpenAs_RunDLL " + lvRecycle.SelectedItems[0].Name, "", ShowCommands.SW_SHOWNORMAL).ToInt32();
    }
    
    Вы, наверно, помните также, что мы сохраняли полное имя объекта в свойстве Name соответствующего ему элемента списка и использовали это в остальном коде.

    Если не найдена ассоциированная с файлом программа, ShellExecute возвращает 31. В этом случае мы вызываем стандартный диалог "Выбор программы", с помощью которого можно выбрать программу для открытия данного файла.

    Я думал, что у меня этот код вызовет ошибку доступа (ведь в Windows 7 невозможно открыть папку $Recycle.Bin на любом диске -- возникает сообщение "Отказано в доступе"). Однако этот код работает, и файлы, находящиеся в корзине, нормально открываются (но вот насчет папок я не проверял).

    Вот, собственно, и всё. Если Вы знаете какие-то API-функции, позволяющие, например, удалить файлы из корзины без показа предупреждающих сообщений, пожалуйста, пишите про них в комментариях.

    UPD:
    После тестирования рассмотренной программы на Windows XP Rus обнаружено, что функции "Восстановить", "Удалить" и "Свойства" не работают, поскольку, например, в этом коде

    private void tsbDelete_Click(object sender, EventArgs e)
    {
       //здесь все без комментариев, т к. код такой же, как и для кнопки "Восстановить"
       shell = new Shell();
       Folder recycler = shell.NameSpace(10);
       foreach (ListViewItem lvi in lvRecycle.SelectedItems)
       {
          foreach (FolderItem2 fi in recycler.Items())
             if ((fi.Name == lvi.Text) && (fi.Path == lvi.Name))
                foreach (FolderItemVerb FIVerb in fi.Verbs())
                   if (FIVerb.Name.ToUpper().Contains("delete".ToUpper()))
                      FIVerb.DoIt();
           lvRecycle.SelectedItems[0].Remove();
       }
       Marshal.FinalReleaseComObject(shell);
    }
    
    fi.Verbs возвращает коллекцию "действий" на русском языке, так, как они отображаются в контекстном меню Проводника. Чтобы программа работала на англоязычной и русскоязычной Windows, строчку

    if (FIVerb.Name.ToUpper().Contains("delete".ToUpper()))
    
    нужно привести к виду
    if ((FIVerb.Name.ToUpper().Contains("delete".ToUpper())) || (FIVerb.Name.ToUpper().Contains("удалить".ToUpper())))
    
    Аналогично и для других языков, нужно только выяснить, где в строке находится символ "_":
    Цитата:Произведя последовательную отладку и используя точки останова, я нашел, что в коллекции действий содержится "RESTORE", у которого вторая буква подчеркнута (как бы перед буквой поставили символ "&"). То есть как бы вся строка до этой буквы обрезается.

    Источник http://www.cyberforum.ru