Как использовать 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);
}
}
