C# 实现 Shell 功能,第二部分






4.89/5 (113投票s)
本文继续介绍使用 C# 进行 Shell 编程。内容包括:启动应用程序、使用正确的 Shell 响应进行 Shell 文件操作(复制、移动、删除)、将文件添加到“最近使用的文档”列表以及进行一些打印机操作。
引言
本文继续探讨如何从 C# 调用 Shell。在本篇文章中,我仍不会涉及“扩展 Shell”的内容,因为 Shell 提供的一些功能我希望先进行回顾。与充满基础细节的第一部分不同,本文相对简单,但我相信您已经阅读了第一篇文章。因此,如果我看到一些在第一部分中解释过的内容,我将不会犹豫提及,但不会再进行解释。
再次建议您先阅读 MSDN 上的以下文章,本文不解释所有 Shell 功能,其他人已经做过,本文的目的是解释如何在 C# 中实现。以下链接是建议的阅读材料:
因此,在本文中,我将讨论如何使用 Shell 在 C# 中启动应用程序、使用 Shell 在 C# 中执行文件操作、使用 Shell 在 C# 中将文件添加到“最近使用的文档”列表以及使用 Shell 在 C# 中执行一些打印机操作。
但是,我将主要解释为什么我不使用 C# 的常规方式来完成所有这些操作。
注意:本文中的代码将使用并扩展在第一部分中编写的代码,我的意思是 ShellLib
类库将会稍微扩展。
主要目标
那么,当 C# 提供了更简单的方式时,我为什么要使用 Shell 的方式来做事呢?嗯,问题在于 C# 没有提供我能获得的所有 Windows 选项。我并不责怪他们,如果您仔细想想,如果 C# 和 .NET 整体旨在成为平台无关的语言,那么它们就无法提供特定平台的选项支持。
让我们回顾一下本文的主要目标:
- 使用不同的动词(打开、编辑、打印)启动应用程序
- 执行带有 Shell 支持的文件操作(回收站、进度条)
- 将文件添加到“最近使用的文档”列表
- 执行打印机管理操作
不再浪费时间,让我们开始工作……
第一部分:启动应用程序
那么,我们这一部分的目标是什么?提供一个简单的类,允许我们根据文件类型(例如,BMP 使用画图,Wave 使用媒体播放器等)启动应用程序。此类还应支持动词。动词是对文件可以执行的操作。每个文件可以有不同的操作,但大多数文件都有“打开”、“编辑”、“属性”等动词。
我们需要做什么来对文件执行这些动词?我们需要使用 ShellExecute
函数,并向其传递文件名和我们想要对文件执行的操作等参数。
所以,首先,我们需要声明 ShellExecute
API。这可以通过以下方式完成:
// Performs an operation on a specified file.
[DllImport("shell32.dll")]
public static extern IntPtr ShellExecute(
IntPtr hwnd, // Handle to a parent window.
[MarshalAs(UnmanagedType.LPTStr)]
String lpOperation, // Pointer to a null-terminated string, referred to in
// this case as a verb, that specifies the action to
// be performed.
[MarshalAs(UnmanagedType.LPTStr)]
String lpFile, // Pointer to a null-terminated string that specifies
// the file or object on which to execute the specified
// verb.
[MarshalAs(UnmanagedType.LPTStr)]
String lpParameters, // If the lpFile parameter specifies an executable file,
// lpParameters is a pointer to a null-terminated string
// that specifies the parameters to be passed to the
// application.
[MarshalAs(UnmanagedType.LPTStr)]
String lpDirectory, // Pointer to a null-terminated string that specifies
// the default directory.
Int32 nShowCmd); // Flags that specify how an application is to be
// displayed when it is opened.
以下是如何使用此函数的示例:
int iRetVal;
iRetVal = (int)ShellLib.ShellApi.ShellExecute(
this.Handle,
"edit",
@"c:\windows\Greenstone.bmp",
"",
Application.StartupPath,
(int)ShellLib.ShellApi.ShowWindowCommands.SW_SHOWNORMAL);
我添加了一个小类,让您可以轻松使用此函数,这是其实现:
public class ShellExecute
{
// Common verbs
public const string OpenFile = "open";
// Opens the file specified by the
// lpFile parameter. The file can
// be an executable file, a document
// file, or a folder.
public const string EditFile = "edit";
// Launches an editor and opens the
// document for editing. If lpFile
// is not a document file, the
// function will fail.
public const string ExploreFolder = "explore";
// Explores the folder specified by
// lpFile.
public const string FindInFolder = "find";
// Initiates a search starting from
// the specified directory.
public const string PrintFile = "print";
// Prints the document file specified
// by lpFile. If lpFile is not a
// document file, the function will
// fail.
// properties
public IntPtr OwnerHandle; // Handle to the owner window
public string Verb; // The requested operation to make on the
// file
public string Path; // String that specifies the file or
// object on which to execute the
// specified verb.
public string Parameters; // String that specifies the parameters to
// be passed to the application.
public string WorkingFolder; // specifies the default directory
public ShellApi.ShowWindowCommands ShowMode;
// Flags that specify how an application
// is to be displayed when it is opened.
public ShellExecute()
{
// Set default values
OwnerHandle = IntPtr.Zero;
Verb = OpenFile;
Path = "";
Parameters = "";
WorkingFolder = "";
ShowMode = ShellApi.ShowWindowCommands.SW_SHOWNORMAL;
}
public bool Execute()
{
int iRetVal;
iRetVal = (int)ShellLib.ShellApi.ShellExecute(
OwnerHandle,
Verb,
Path,
Parameters,
WorkingFolder,
(int)ShowMode);
return (iRetVal > 32) ? true : false;
}
}
以下是如何使用该类:
ShellLib.ShellExecute shellExecute = new ShellLib.ShellExecute();
shellExecute.Verb = ShellLib.ShellExecute.EditFile;
shellExecute.Path = @"c:\windows\Coffee Bean.bmp";
shellExecute.Execute();
注意:其中一些功能可以通过 Process
和 ProcessStartInfo
类来实现。但我们本文的目标是使用 Shell 函数,这能提供更好的灵活性。
第二部分:执行文件操作
这些文件操作有什么作用?我们将在本节中复制文件的方式与常规方式有什么区别?嗯,常规方式通常是使用属于文件存储 API 集的 API 函数:CopyFile
、MoveFile
、DeleteFile
。这些函数可以完成工作,但 Shell 函数还可以为您提供该文件的 Shell 支持,这意味着使用 Shell 函数,您可以在复制操作时看到进度对话框,可以将删除的文件移至回收站。您可以简单地撤销您的操作。而且,当您通过资源管理器执行这些操作时,还会获得所有漂亮的对话框。
那么,它是如何完成的?使用 SHFileOperation
API 函数,此操作接收一个包含操作所需所有信息的 struct
,包括源和目标、特殊标志等。该函数的 C# 声明如下:
// Copies, moves, renames, or deletes a file system object.
[DllImport("shell32.dll" , CharSet = CharSet.Unicode)]
public static extern Int32 SHFileOperation(
ref SHFILEOPSTRUCT lpFileOp); // Address of an SHFILEOPSTRUCT
// structure that contains information this function needs
// to carry out the specified operation. This parameter must
// contain a valid value that is not NULL. You are
// responsible for validating the value. If you do not
// validate it, you will experience unexpected results.
从函数声明可以看出,它期望一个名为 SHFILEOPSTRUCT
的结构,其定义如下:
// Contains information that the SHFileOperation function uses to perform
// file operations.
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
public IntPtr hwnd; // Window handle to the dialog box to display
// information about the status of the file
// operation.
public UInt32 wFunc; // Value that indicates which operation to
// perform.
public IntPtr pFrom; // Address of a buffer to specify one or more
// source file names. These names must be
// fully qualified paths. Standard Microsoft®
// MS-DOS® wild cards, such as "*", are
// permitted in the file-name position.
// Although this member is declared as a
// null-terminated string, it is used as a
// buffer to hold multiple file names. Each
// file name must be terminated by a single
// NULL character. An additional NULL
// character must be appended to the end of
// the final name to indicate the end of pFrom.
public IntPtr pTo; // Address of a buffer to contain the name of
// the destination file or directory. This
// parameter must be set to NULL if it is not
// used. Like pFrom, the pTo member is also a
// double-null terminated string and is handled
// in much the same way.
public UInt16 fFlags; // Flags that control the file operation.
public Int32 fAnyOperationsAborted;
// Value that receives TRUE if the user aborted
// any file operations before they were
// completed, or FALSE otherwise.
public IntPtr hNameMappings;
// A handle to a name mapping object containing
// the old and new names of the renamed files.
// This member is used only if the
// fFlags member includes the
// FOF_WANTMAPPINGHANDLE flag.
[MarshalAs(UnmanagedType.LPWStr)]
public String lpszProgressTitle;
// Address of a string to use as the title of
// a progress dialog box. This member is used
// only if fFlags includes the
// FOF_SIMPLEPROGRESS flag.
}
那么,这里有什么?首先,我们有 hwnd
,这是所有者窗口的句柄。与许多 Shell API 函数一样,Shell 经常会显示用户界面对话框以获取一些输入,因此您需要指定哪个窗口将是这些对话框的所有者。
接下来是 wFunc
值,该值设置我们感兴趣的操作。选项包括 Copy
(复制)、Move
(移动)和 Delete
(删除)。事实上,还有一个选项是 Rename
(重命名),但它非常有限,您可以通过 Move
操作获得相同的效果,所以我不会处理它。
pFrom
参数是设置源文件的参数,而 pTo
参数是设置目标文件的参数。现在您可能想知道如何将字符串列表准确地放入单个 IntPtr
。嗯,这不是 string
数组。设计这些 API 的家伙使用了一种特殊的技术来存储字符串列表。他们将它们存储在一个大字符串中,每个字符串之间用 NULL
字符分隔,并在字符串末尾用双 NULL
字符。所以,假设我们想复制两个文件:“c:\file.txt”和“c:\file2.txt”,所需的 pFrom
字符串应该是:"c:\file.txt" + "\0" + "c:\file2.txt" + "\0\0"
。是的,这确实很奇怪,但这就是为什么 pFrom
参数不能作为普通 string
封送的原因,相反,我需要使用 Marshal.StringToHGlobalUni
,它会在本机堆上提供一个字符串副本的指针。
接下来是 fFlags
参数。此参数允许我们控制文件操作的某些方面。我们可以设置标志来实现静默模式(不显示进度对话框),我们可以设置“全部是”作为任何将显示的对话框的响应,我们可以避免在用户发生错误时显示错误对话框,等等。
其余的标志不太有趣。fAnyOperationAborted
是 ShFileOperation
函数用于指示操作是否被中止的地方。hNameMappings
很少使用,只有在我对用户在操作过程中必须提供的姓名感兴趣时才有帮助。最后是 lpszProgressTitle
,如果我们设置了 FOF_SIMPLEPROGRESS
标志,进度对话框就不会显示文件名,而是应该显示此参数的文本。当我测试此函数时,我无法显示此参数。使用 SIMPLEPROGRESS
标志时,它不显示文件名,但也不显示标题参数。我能说什么呢,很奇怪。
现在我们知道了如何操作,我们将看到我所做的类,它以一种方便的方式将其封装起来。该类名为 ShellFileOperation
。它包含两个 enum
来使生活更轻松,称为 FileOperations
和 ShellFileOperationFlags
。该类具有以下属性:
// properties
public FileOperations Operation;
public IntPtr OwnerWindow;
public ShellFileOperationFlags OperationFlags;
public String ProgressTitle;
public String[] SourceFiles;
public String[] DestFiles;
我认为这里不需要解释。此外,该类中还有一个小型辅助函数,它接收一个 string
数组并返回一个先前提到的格式的 string
(双 null
终止字符串),这是完成这项工作的代码:
private String StringArrayToMultiString(String[] stringArray)
{
String multiString = "";
if (stringArray == null)
return "";
for (int i=0 ; i<stringArray.Length ; i++)
multiString += stringArray[i] + '\0';
multiString += '\0';
return multiString;
}
最后,类中最重要的函数是 DoOperation
,它创建一个新的 struct
,设置其字段,获取我们特殊 From
和 To string
的堆内存指针,并使用 struct
调用 SHFileOperation
函数。代码如下:
public bool DoOperation()
{
ShellApi.SHFILEOPSTRUCT FileOpStruct = new ShellApi.SHFILEOPSTRUCT();
FileOpStruct.hwnd = OwnerWindow;
FileOpStruct.wFunc = (uint)Operation;
String multiSource = StringArrayToMultiString(SourceFiles);
String multiDest = StringArrayToMultiString(DestFiles);
FileOpStruct.pFrom = Marshal.StringToHGlobalUni(multiSource);
FileOpStruct.pTo = Marshal.StringToHGlobalUni(multiDest);
FileOpStruct.fFlags = (ushort)OperationFlags;
FileOpStruct.lpszProgressTitle = ProgressTitle;
FileOpStruct.fAnyOperationsAborted = 0;
FileOpStruct.hNameMappings = IntPtr.Zero;
int RetVal;
RetVal = ShellApi.SHFileOperation(ref FileOpStruct);
ShellApi.SHChangeNotify(
(uint)ShellChangeNotificationEvents.SHCNE_ALLEVENTS,
(uint)ShellChangeNotificationFlags.SHCNF_DWORD,
IntPtr.Zero,
IntPtr.Zero);
if (RetVal!=0)
return false;
if (FileOpStruct.fAnyOperationsAborted != 0)
return false;
return true;
}
是的,我知道,我没有提到 SHChangeNotify
。虽然您不一定需要使用此函数,但在应用程序对文件系统进行某些更改后,建议通知 Shell 这些更改,以便它能够根据更改进行更新。SHChangeNotify
就是这样做的方式,这是另一个 Shell API 函数,它的作用是通知 Shell,仅此而已。它接收发生的事件(有一个 enum
),以及两个参数,具体取决于事件。
以下是使用此类的一个示例:以下示例使用 Shell 复制将 winmine.exe、freecell.exe 和 mshearts.exe 这三个文件从 system 目录复制到 root 目录。代码第一次运行时,会显示一个进度对话框,第二次运行时,还会询问您是否要覆盖现有文件……只需想想为了处理文件 IO 操作中所有可能的失败情况,您需要编写多少代码。
ShellLib.ShellFileOperation fo = new ShellLib.ShellFileOperation();
String[] source = new String[3];
String[] dest = new String[3];
source[0] = Environment.SystemDirectory + @"\winmine.exe";
source[1] = Environment.SystemDirectory + @"\freecell.exe";
source[2] = Environment.SystemDirectory + @"\mshearts.exe";
dest[0] = Environment.SystemDirectory.Substring(0,2) + @"\winmine.exe";
dest[1] = Environment.SystemDirectory.Substring(0,2) + @"\freecell.exe";
dest[2] = Environment.SystemDirectory.Substring(0,2) + @"\mshearts.exe";
fo.Operation = ShellLib.ShellFileOperation.FileOperations.FO_COPY;
fo.OwnerWindow = this.Handle;
fo.SourceFiles = source;
fo.DestFiles = dest;
bool RetVal = fo.DoOperation();
if (RetVal)
MessageBox.Show("Copy Complete without errors!");
else
MessageBox.Show("Copy Complete with errors!");
进入下一部分。
第三部分:将文件添加到“最近使用的文档”列表
“最近使用的文档”列表是一个特殊文件夹,您可以使用在第一部分中介绍的 SHGetFolderLocation
或 SHGetFolderPath
API 来查找它。但如果您想对该目录进行更改,则不应直接进行,因为更改不会正确更新,也不会反映在开始菜单中。相反,要以适当的方式进行更改,您应该使用 API 函数 SHAddToRecentDocs
。所以这个 API 的样子是这样的:
// Adds a document to the Shell's list of recently used documents or clears all
// documents from the list.
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
UInt32 uFlags, // Flag that indicates the meaning of the pv parameter.
IntPtr pv); // A pointer to either a null-terminated string with the
// path and file name of the document, or a PIDL that
// identifies the document's file object. Set this parameter
// to NULL to clear all documents from the list.
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
UInt32 uFlags,
[MarshalAs(UnmanagedType.LPWStr)]
String pv);
不,这不是错误,该 API 有两个声明。第一个参数可以是两个值之一:SHARD_PIDL
或 SHARD<CODE>_PATH
(实际上,第二个值有 ANSI 和 Unicode 版本)。如果您使用 PIDL 标志激活该函数,则表示第二个参数 pv
将包含您要添加的文件的 PIDL
。但是,如果将 PATH
标志放入 flags
参数中,则表示第二个参数是一个 string
。因此,由于第二个参数有时可能是 IntPtr
(当您使用 PIDL
标志时),有时也可能是 string
(当您使用 PATH
标志时),我为此 API 编写了两个声明。
此外,我还为此函数编写了一个小型包装类。它包含可能的标志的 enum
和两个 static
方法,一个用于向文档列表添加新项,一个用于清除列表。这是该类的代码:
public class ShellAddRecent
{
public enum ShellAddRecentDocs
{
SHARD_PIDL = 0x00000001, // The pv parameter points to a
// null-terminated string with the path
// and file name of the object.
SHARD_PATHA = 0x00000002, // The pv parameter points to a pointer
// to an item identifier list (PIDL)
// that identifies the document's file
// object. PIDLs that identify nonfile
// objects are not allowed.
SHARD_PATHW = 0x00000003 // same as SHARD_PATHA but unicode string
}
public static void AddToList(String path)
{
ShellApi.SHAddToRecentDocs((uint)ShellAddRecentDocs.SHARD_PATHW,path);
}
public static void ClearList()
{
ShellApi.SHAddToRecentDocs((uint)ShellAddRecentDocs.SHARD_PIDL,
IntPtr.Zero);
}
}
这里没有什么需要解释的。需要注意的一点是,如果您想清除列表,只需在 SHAddToRecentDocs
函数的第二个参数中放置一个 null
即可。以下是如何使用该类:
ShellLib.ShellAddRecent.AddToList(@"c:\windows\Rhododendron.bmp");
ShellLib.ShellAddRecent.ClearList();
第四部分:管理打印机
还记得第一部分吗?关于 ShellExecute
的内容?还有动词?所以,如果您想打印可打印的内容,例如 Word 文档或位图,您只需要使用带有“print
”动词的 Shell Execute 命令。我知道这很简单。但是,您还可以通过一个名为 SHInvokePrinterCommand
的 API 执行更多打印机操作。这是其 C# 声明:
// Executes a command on a printer object.
[DllImport("shell32.dll")]
public static extern Int32 SHInvokePrinterCommand(
IntPtr hwnd, // Handle of the window that will be used as the parent
// of any windows or dialog boxes that are created
// during the operation.
UInt32 uAction, // A value that determines the type of printer
// operation that will be performed.
[MarshalAs(UnmanagedType.LPWStr)]
String lpBuf1, // Address of a null-terminated string that contains
// additional information for the printer command.
[MarshalAs(UnmanagedType.LPWStr)]
String lpBuf2, // Address of a null-terminated string that contains
// additional information for the printer command.
Int32 fModal); // value that determines whether
// SHInvokePrinterCommand should return after
// initialising the command or wait until the command
// is completed.
第一个参数是所有者窗口句柄,第二个参数 uAction
是您要执行的命令类型,您可以从 PrinterActions enum
值中选择一个。lpBuf1
和 lpBuf2
是取决于所执行操作的两个参数。最后一个参数 fModal
设置函数是等待命令执行完毕再返回,还是立即返回。以下是使用此函数的三个示例:
打开打印机
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_OPEN,
"printer name comes here",
"",
1);
显示打印机属性
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_PROPERTIES,
"printer name comes here",
"",
1);
打印测试页
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_TESTPAGE,
"printer name comes here",
"",
1);
您可以看到,非常简单。不需要包装类。
额外说明
嗯,我曾考虑过创建一个关于使用 Shell 进行拖放功能的章节,但我发现 C# 本身已经支持了所有这些功能,所以使用 Shell API 将是完全浪费时间。
这完成了“使用 Shell”部分。下一篇文章将是关于“扩展 Shell”的内容,它允许您做非常有趣的事情。请注意,我将跳过一些 MSDN 文章,这些文章仅通过注册表操作来讨论“扩展”Shell。如果您想了解如何做到这一点,您应该访问以下 MSDN 链接(不涉及编程):Shell 基础:扩展 Shell。
更新(2010 年 1 月 9 日)
事实证明,我对 SHFILEOPSTRUCT
结构的定义存在一个 bug,导致其中一些提到的功能无法正常工作。
代码已更新如下:ShellNameMapping.cs
- 新类/文件已添加
ShellFileOperation.cs:
- 添加了
NameMappings
属性和处理代码,以便在复制/移动操作后填充此属性。
ShellApi.cs:
- 添加了
SHNAMEMAPPINGSTRUCT
、SHNAMEMAPPINGINDEXSTRUCT
和SHFreeNameMappings()
来接收NameMappings
。 - 添加了
SHFILEOPSTRUCT32
、SHFILEOPSTRUCT64
、SHFileOperation32()
、SHFileOperation64()
声明。 - 将
SHFileOperation()
改为代理,该代理根据目标机器调用 32 位或 64 位版本。 - 添加了
GetMachineType()
。
前三个更改在这里进行了描述。
后三个对于在 32 位和 64 位机器上运行而不崩溃至关重要。
新代码在 ShellFileOperation
类上提供了一个额外的 NameMappings
属性,该属性在复制/移动操作后填充。通过此属性,可以获取用户在文件操作期间必须提供的姓名(例如,如果目标文件在 Vista 或 Windows 7 上已存在,则可以将其复制并更改名称)。此外,代码现在可以在 32 位和 64 位机器上运行。
功劳归于 Wolfram Bernhardt 和 Benjamin Schröter,他们发现了并修复了这个 bug。
就是这样!
希望您喜欢。别忘了投票。
历史
- 2010 年 1 月 12 日:初始版本