65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (113投票s)

2003 年 1 月 31 日

Ms-PL

11分钟阅读

viewsIcon

453770

downloadIcon

12099

本文继续介绍使用 C# 进行 Shell 编程。内容包括:启动应用程序、使用正确的 Shell 响应进行 Shell 文件操作(复制、移动、删除)、将文件添加到“最近使用的文档”列表以及进行一些打印机操作。

Sample Image - csdoesshell2.jpg

引言

本文继续探讨如何从 C# 调用 Shell。在本篇文章中,我仍不会涉及“扩展 Shell”的内容,因为 Shell 提供的一些功能我希望先进行回顾。与充满基础细节的第一部分不同,本文相对简单,但我相信您已经阅读了第一篇文章。因此,如果我看到一些在第一部分中解释过的内容,我将不会犹豫提及,但不会再进行解释。

再次建议您先阅读 MSDN 上的以下文章,本文不解释所有 Shell 功能,其他人已经做过,本文的目的是解释如何在 C# 中实现。以下链接是建议的阅读材料:

因此,在本文中,我将讨论如何使用 Shell 在 C# 中启动应用程序、使用 Shell 在 C# 中执行文件操作、使用 Shell 在 C# 中将文件添加到“最近使用的文档”列表以及使用 Shell 在 C# 中执行一些打印机操作。

但是,我将主要解释为什么我不使用 C# 的常规方式来完成所有这些操作。

注意:本文中的代码将使用并扩展在第一部分中编写的代码,我的意思是 ShellLib 类库将会稍微扩展。

主要目标

那么,当 C# 提供了更简单的方式时,我为什么要使用 Shell 的方式来做事呢?嗯,问题在于 C# 没有提供我能获得的所有 Windows 选项。我并不责怪他们,如果您仔细想想,如果 C# 和 .NET 整体旨在成为平台无关的语言,那么它们就无法提供特定平台的选项支持。

让我们回顾一下本文的主要目标:

  1. 使用不同的动词(打开、编辑、打印)启动应用程序
  2. 执行带有 Shell 支持的文件操作(回收站、进度条)
  3. 将文件添加到“最近使用的文档”列表
  4. 执行打印机管理操作

不再浪费时间,让我们开始工作……

第一部分:启动应用程序

那么,我们这一部分的目标是什么?提供一个简单的类,允许我们根据文件类型(例如,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();

注意:其中一些功能可以通过 ProcessProcessStartInfo 类来实现。但我们本文的目标是使用 Shell 函数,这能提供更好的灵活性。

第二部分:执行文件操作

这些文件操作有什么作用?我们将在本节中复制文件的方式与常规方式有什么区别?嗯,常规方式通常是使用属于文件存储 API 集的 API 函数:CopyFileMoveFileDeleteFile。这些函数可以完成工作,但 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 参数。此参数允许我们控制文件操作的某些方面。我们可以设置标志来实现静默模式(不显示进度对话框),我们可以设置“全部是”作为任何将显示的对话框的响应,我们可以避免在用户发生错误时显示错误对话框,等等。

其余的标志不太有趣。fAnyOperationAbortedShFileOperation 函数用于指示操作是否被中止的地方。hNameMappings 很少使用,只有在我对用户在操作过程中必须提供的姓名感兴趣时才有帮助。最后是 lpszProgressTitle,如果我们设置了 FOF_SIMPLEPROGRESS 标志,进度对话框就不会显示文件名,而是应该显示此参数的文本。当我测试此函数时,我无法显示此参数。使用 SIMPLEPROGRESS 标志时,它不显示文件名,但也不显示标题参数。我能说什么呢,很奇怪。

现在我们知道了如何操作,我们将看到我所做的类,它以一种方便的方式将其封装起来。该类名为 ShellFileOperation。它包含两个 enum 来使生活更轻松,称为 FileOperationsShellFileOperationFlags。该类具有以下属性:

// 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,设置其字段,获取我们特殊 FromTo 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.exefreecell.exemshearts.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!");

进入下一部分。

第三部分:将文件添加到“最近使用的文档”列表

“最近使用的文档”列表是一个特殊文件夹,您可以使用在第一部分中介绍的 SHGetFolderLocationSHGetFolderPath 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_PIDLSHARD<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 值中选择一个。lpBuf1lpBuf2 是取决于所执行操作的两个参数。最后一个参数 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:

  • 添加了 SHNAMEMAPPINGSTRUCTSHNAMEMAPPINGINDEXSTRUCTSHFreeNameMappings() 来接收 NameMappings
  • 添加了 SHFILEOPSTRUCT32SHFILEOPSTRUCT64SHFileOperation32()SHFileOperation64() 声明。
  • SHFileOperation() 改为代理,该代理根据目标机器调用 32 位或 64 位版本。
  • 添加了 GetMachineType()

前三个更改在这里进行了描述。

后三个对于在 32 位和 64 位机器上运行而不崩溃至关重要。

新代码在 ShellFileOperation 类上提供了一个额外的 NameMappings 属性,该属性在复制/移动操作后填充。通过此属性,可以获取用户在文件操作期间必须提供的姓名(例如,如果目标文件在 Vista 或 Windows 7 上已存在,则可以将其复制并更改名称)。此外,代码现在可以在 32 位和 64 位机器上运行。

功劳归于 Wolfram Bernhardt 和 Benjamin Schröter,他们发现了并修复了这个 bug。

就是这样!

希望您喜欢。别忘了投票。

历史

  • 2010 年 1 月 12 日:初始版本
© . All rights reserved.