SHFileOperation и c#

Как использовать SHFileOperation в .NET. На самом деле все просто.

Итак, во-первых нужно объявить структуру SHFILEOPSTRUCT. Это определение из MSDN:

typedef struct _SHFILEOPSTRUCT {
HWND         hwnd;
UINT         wFunc;
LPCTSTR      pFrom;
LPCTSTR      pTo;
FILEOP_FLAGS fFlags;
BOOL         fAnyOperationsAborted;
LPVOID       hNameMappings;
LPCTSTR      lpszProgressTitle;
} SHFILEOPSTRUCT, *LPSHFILEOPSTRUCT;
На первый взгляд декларация должна быть такой:
//это плохое объявление
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct SHFILEOPSTRUCT
{
    internal IntPtr hwnd;
    internal int wFunc;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pFrom;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pTo;
    internal ushort fFlags;
    internal bool fAnyOperationsAborted;
    internal IntPtr hNameMappings;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string lpszProgressTitle;
}
и на 64-битной платформе оно будет работать, но на 32-битной системе оно не прокатывает. Внимательно читаем Shellapi.h и видим
#if !defined(_WIN64)
#include <pshpack1.h>#endif
Вот оно тяжелое наследие. Это означает, что, если программа на с++ будет собираться под платформу, отличную от 64-битной, то структура SHFILEOPSTRUCT будет иметь 1-байтовое выравнивание, в противном случае - автоматическое выравнивание. В нашем случае придется объявлять два варианта с разным выравниванием. С перечислением wFunc и флагом fFlags все просто: берем константы из shellapi.h и превращаем их для удобства в enum'ы.
public enum ShFileOperationFunc
{
    FO_MOVE = 0x0001,
    FO_COPY = 0x0002,
    FO_DELETE = 0x0003,
    FO_RENAME = 0x0004
}

public enum ShFileOperationFlag : ushort
{
    FOF_MULTIDESTFILES = 0x0001,
    /// <summary>
    /// Не используется
    /// </summary>
    FOF_CONFIRMMOUSE = 0x0002,
    /// <summary>
    /// не показывать прогресс
    /// </summary>
    FOF_SILENT = 0x0004,
    /// <summary>
    /// переименовывать файлы в случае совпадения имен
    /// </summary>
    FOF_RENAMEONCOLLISION = 0x0008,
    /// <summary>
    /// не показывать диалоги подтверждения
    /// </summary>
    FOF_NOCONFIRMATION = 0x0010,
    /// <summary>
    /// Заполнять поле SHFILEOPSTRUCT.hNameMappings, нужно освобождать вызовом SHFreeNameMappings
    /// </summary>
    FOF_WANTMAPPINGHANDLE = 0x0020,
    /// <summary>
    /// включает возможность отмены операций, в том числе удаление в корзину
    /// </summary>
    FOF_ALLOWUNDO = 0x0040,
    /// <summary>
    /// обрабатывать только файлы (но не директории)
    /// </summary>
    FOF_FILESONLY = 0x0080,
    /// <summary>
    /// не показывать имена файлов в окне прогресса
    /// </summary>
    FOF_SIMPLEPROGRESS = 0x0100,
    /// <summary>
    /// не показывать подтверждения для создания новых директорий
    /// </summary>
    FOF_NOCONFIRMMKDIR = 0x0200,
    /// <summary>
    /// не показывать сообщение об ошибке
    /// </summary>
    FOF_NOERRORUI = 0x0400,
    /// <summary>
    /// не копировать атрибуты безопасности
    /// </summary>
    FOF_NOCOPYSECURITYATTRIBS = 0x0800,
    /// <summary>
    /// не обходить рекурсивно директории (при удалении один хрен - не пустые директории все равно удаляются)
    /// </summary>
    FOF_NORECURSION = 0x1000,
    /// <summary>
    /// не обрабатывать присоединенные элементы (типа директорий XXX_files, которые создаются вместе с XXX.htm)
    /// </summary>
    FOF_NO_CONNECTED_ELEMENTS = 0x2000,
    /// <summary>
    /// предупреждать при безвозвратном удалении (перекрывает FOF_NOCONFIRMATION)
    /// </summary>
    FOF_WANTNUKEWARNING = 0x4000,
    /// <summary>
    /// устарело
    /// </summary>
    FOF_NORECURSEREPARSE = 0x8000,
    /// <summary>
    /// не показывать пользователю ничего
    /// </summary>
    FOF_NO_UI = 0x0004 | 0x0010 | 0x0400 | 0x0200
}
Теперь структуры выглядят так
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
internal struct SHFILEOPSTRUCT_32
{
// 32 битная структура
    internal IntPtr hwnd;
    internal ShFileOperationFunc wFunc;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pFrom;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pTo;
    internal ShFileOperationFlag fFlags;
    internal bool fAnyOperationsAborted;
    internal IntPtr hNameMappings;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string lpszProgressTitle;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct SHFILEOPSTRUCT_64
{
// 64 битная структура
    internal IntPtr hwnd;
    internal ShFileOperationFunc wFunc;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pFrom;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string pTo;
    internal ShFileOperationFlag fFlags;
    internal bool fAnyOperationsAborted;
    internal IntPtr hNameMappings;
    [MarshalAs(UnmanagedType.LPTStr)]
    internal string lpszProgressTitle;
}
нужно помнить, что поля pFrom и pTo это не совсем строки (опять тяжелое наследие), а как бы массивы строк. Они должны состоять из отдельных строк, разделенных символом '\0' и завершаться дополнительным '\0', показывающим конец. То есть в любом случае завершающих нуль-символов должно быть два. Составляем pFrom и pTo так:
internal static string build_shell_string(string[] inp)
{
    if ((inp == null) || (inp.Length == 0))
    {
        return null;
    }
    StringBuilder sb=new StringBuilder();
    for (int i=0; i < inp.Length; i++)
    {
        sb.Append(inp[i]);
        sb.Append('\0');
    }
    //завешающий \0 добавит маршализатор
    return sb.ToString();
}
Определение самой функции:
[DllImport("shell32.dll", CharSet = CharSet.Auto, EntryPoint = "SHFileOperation", SetLastError = false)]
internal static extern int SHFileOperation32
(ref SHFILEOPSTRUCT_32 lpFileOp);

[DllImport("shell32.dll", CharSet = CharSet.Auto, EntryPoint = "SHFileOperation", SetLastError = false)]
internal static extern int SHFileOperation64
(ref SHFILEOPSTRUCT_64 lpFileOp);
Теперь разберемся с возвращаемым значением. Если отлично от нуля, значит произошла ошибка, причем MSDN велит не вызывать в этом случае GetLastError, а анализировать результат. Возвращается одна из констант ERROR_ из winerror.h или "до-win32" коды ошибок, причем вторые перекрывают первые. Таким образом для человеческой обработки ошибок понадобится сопоставить "до-win32" коды ошибок и коды ERROR_. Это слегка модифицированный код из far_uncode:
private static Win32Exception SHErrorToException(int SHError)
{
    int WinError = SHError;
    switch (SHError)
    {
        case 0x71:
            WinError = ERROR_ALREADY_EXISTS; break;
            // DE_SAMEFILE The source and destination files are the same file.
        case 0x72:
            WinError = ERROR_INVALID_PARAMETER; break;
            // DE_MANYSRC1DEST Multiple file paths were specified in the source buffer, but only one destination file path.
        case 0x73:
            WinError = ERROR_NOT_SAME_DEVICE; break;
            // DE_DIFFDIR Rename operation was specified but the destination path is a different directory. Use the move operation instead.
        case 0x74:
            WinError = ERROR_ACCESS_DENIED; break;
            // DE_ROOTDIR The source is a root directory, which cannot be moved or renamed.
         case 0x75:
            WinError = ERROR_CANCELLED; break;
            // DE_OPCANCELLED The operation was cancelled by the user, or silently cancelled if the appropriate flags were supplied to SHFileOperation.
         case 0x76:
            WinError = ERROR_BAD_PATHNAME; break;
            // DE_DESTSUBTREE The destination is a subtree of the source.
         case 0x78:
            WinError = ERROR_ACCESS_DENIED; break;
            // DE_ACCESSDENIEDSRC Security settings denied access to the source.
         case 0x79:
            WinError = ERROR_BUFFER_OVERFLOW; break;
            // DE_PATHTOODEEP The source or destination path exceeded or would exceed MAX_PATH.
         case 0x7A:
            WinError = ERROR_INVALID_PARAMETER; break;
            // DE_MANYDEST The operation involved multiple destination paths, which can fail in the case of a move operation.
         case 0x7C:
            WinError = ERROR_BAD_PATHNAME; break;
            // DE_INVALIDFILES The path in the source or destination or both was invalid.
         case 0x7D:
            WinError = ERROR_INVALID_PARAMETER; break;
            // DE_DESTSAMETREE The source and destination have the same parent folder.
         case 0x7E:
            WinError = ERROR_ALREADY_EXISTS; break;
            // DE_FLDDESTISFILE The destination path is an existing file.
         case 0x80:
            WinError = ERROR_ALREADY_EXISTS; break;
            // DE_FILEDESTISFLD The destination path is an existing folder.
         case 0x81:
            WinError = ERROR_BUFFER_OVERFLOW; break;
            // DE_FILENAMETOOLONG The name of the file exceeds MAX_PATH.
         case 0x82:
            WinError = ERROR_WRITE_FAULT; break;
            // DE_DEST_IS_CDROM The destination is a read-only CD-ROM, possibly unformatted.
         case 0x83:
            WinError = ERROR_WRITE_FAULT; break;
            // DE_DEST_IS_DVD The destination is a read-only DVD, possibly unformatted.
         case 0x84:
            WinError = ERROR_WRITE_FAULT; break;
            // DE_DEST_IS_CDRECORD The destination is a writable CD-ROM, possibly unformatted.
         case 0x85:
            WinError = ERROR_DISK_FULL; break;
            // DE_FILE_TOO_LARGE The file involved in the operation is too large for the destination media or file system.
         case 0x86:
            WinError = ERROR_READ_FAULT; break;
            // DE_SRC_IS_CDROM The source is a read-only CD-ROM, possibly unformatted.
         case 0x87:
            WinError = ERROR_READ_FAULT; break;
            // DE_SRC_IS_DVD The source is a read-only DVD, possibly unformatted.
         case 0x88:
            WinError = ERROR_READ_FAULT; break;
            // DE_SRC_IS_CDRECORD The source is a writable CD-ROM, possibly unformatted.
         case 0xB7:
            WinError = ERROR_BUFFER_OVERFLOW; break;
            // DE_ERROR_MAX MAX_PATH was exceeded during the operation.
         case 0x402:
            WinError = ERROR_PATH_NOT_FOUND; break;
            // An unknown error occurred. This is typically due to an invalid path in the source or destination. This error does not occur on Windows Vista and later.
         case 0x10000:
            WinError = ERROR_GEN_FAILURE; break;
            // ERRORONDEST An unspecified error occurred on the destination.
    }

    return new Win32Exception(WinError);
}
Теперь осталось объединить всё написанное ранее в одну обертку
internal static int ShellFileOperation32
    (string[] from,
    string[] to,
    ShFileOperationFunc func,
    ShFileOperationFlag opts,
    IntPtr hwnd,
    string progress_title)
{
//приготовляем аргумент
    SHFILEOPSTRUCT_32 arg=new SHFILEOPSTRUCT_32();
    arg.fAnyOperationsAborted = false;
    arg.fFlags = opts;
    arg.hNameMappings = IntPtr.Zero;
    arg.hwnd = hwnd;
    arg.lpszProgressTitle = progress_title;
    arg.pFrom = build_shell_string(from);
    arg.pTo = build_shell_string(to);
    arg.wFunc = func;

    return SHFileOperation32(ref arg);
}

internal static int ShellFileOperation64
    (string[] from,
    string[] to,
    ShFileOperationFunc func,
    ShFileOperationFlag opts,
    IntPtr hwnd,string progress_title)
{
//приготовляем аргумент
    SHFILEOPSTRUCT_64 arg=new SHFILEOPSTRUCT_64();
    arg.fAnyOperationsAborted = false;
    arg.fFlags = opts;
    arg.hNameMappings = IntPtr.Zero;
    arg.hwnd = hwnd;
    arg.lpszProgressTitle = progress_title;
    arg.pFrom = build_shell_string(from);
    arg.pTo = build_shell_string(to);
    arg.wFunc = func;

    return SHFileOperation64(ref arg);
}

/// <summary>
/// обертка для SHFileOperation
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="func"></param>
/// <param name="opts"></param>
/// <param name="hwnd">указатель на родительское окно для показа диалогов</param>
/// <param name="progress_title">заголовок окна прогресса, если установлено FOF_SIMPLEPROGRESS</param>
public static void ShellFileOperation
    (string[] from,
    string[] to,
    ShFileOperationFunc func,
    ShFileOperationFlag opts,
    IntPtr hwnd,
    string progress_title)
{
    int shell_res=0;
    if (IntPtr.Size == 4)
    {
    //32 бита
        shell_res = ShellFileOperation32(from, to, func, opts, hwnd, progress_title);
    }
    else
    {
    //64 бита
        shell_res = ShellFileOperation64(from, to, func, opts, hwnd, progress_title);
    }
    if (shell_res != 0)
    {
        throw SHErrorToException(shell_res);
    }
}