Introduction
This article continues to explore how to use the shell from C#. In this article, I will still not touch on 'extending the shell', because there is some functionality the shell gives us that I want to review first. Unlike Part 1, which was full of basic details, this article is quite simple, but I do trust you read the first article. So in case I see something I've explained in part 1, I won't hesitate to mention it and not explain it.
Again, I suggest the following articles from MSDN will be read first, the article does not explain all the shell functionality, others have done this already, the article comes to explain how to do it in C#. The following links are suggested reading material:
So, in this article, I'll discuss launching application in C# with the shell, doing file operations in C# using the shell, adding files to the Recent Document list in C# using the shell and doing some printer operations in C# using the shell.
But, the main thing I'll explain is why I don't use the normal C# way to do all this.
Note, the code in this article will use and extend the code written in Part 1, I mean the ShellLib
class library will grow a bit.
Main Goals
So, why should I use the shell way to do stuff when C# offers me a simpler way? Well, the problem is that C# does not give me all the Windows options I can get. I don't blame them, if you think about it, if C# and .NET in general are supposed to be platform independent languages, they can't give support for specific platform options.
Let's review our main goals of this article:
- Launching applications with different kind of verbs (open, edit, print)
- Doing file operation with the shell support (Recycle bin, progress bar)
- Adding files to the Recent Document list
- Doing printer management operations
Enough wasting time, let's get to work...
Section 1: Launching Applications
So, what is our objective in this section? To provide a simple class that lets us launch an application according to their file types (paint for BMP, media player for wave, etc.). This class should also support verbs. Verbs are operations that can be done on a file. Each file can have different operations, but most files have the 'open' verb, 'edit' verb, 'properties' verb and more.
What do we need to do to perform these verbs on a file? We need to use the function ShellExecute
, and pass it the parameters like file name and what operation we want to perform on the file.
So first, we need to declare the ShellExecute
API. This is done as follows:
[DllImport("shell32.dll")]
public static extern IntPtr ShellExecute(
IntPtr hwnd,
[MarshalAs(UnmanagedType.LPTStr)]
String lpOperation,
[MarshalAs(UnmanagedType.LPTStr)]
String lpFile,
[MarshalAs(UnmanagedType.LPTStr)]
String lpParameters,
[MarshalAs(UnmanagedType.LPTStr)]
String lpDirectory,
Int32 nShowCmd);
Here is an example for using this function:
int iRetVal;
iRetVal = (int)ShellLib.ShellApi.ShellExecute(
this.Handle,
"edit",
@"c:\windows\Greenstone.bmp",
"",
Application.StartupPath,
(int)ShellLib.ShellApi.ShowWindowCommands.SW_SHOWNORMAL);
I've added a small class that lets you use this function easily, here is the implementation:
public class ShellExecute
{
public const string OpenFile = "open";
public const string EditFile = "edit";
public const string ExploreFolder = "explore";
public const string FindInFolder = "find";
public const string PrintFile = "print";
public IntPtr OwnerHandle;
public string Verb;
public string Path;
public string Parameters;
public string WorkingFolder;
public ShellApi.ShowWindowCommands ShowMode;
public ShellExecute()
{
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;
}
}
And here is how you use the class:
ShellLib.ShellExecute shellExecute = new ShellLib.ShellExecute();
shellExecute.Verb = ShellLib.ShellExecute.EditFile;
shellExecute.Path = @"c:\windows\Coffee Bean.bmp";
shellExecute.Execute();
Note: Some of this functionality can be achieved using the Process
and ProcessStartInfo
classes. But our goal in this article is using the shell functions, which allow us better flexibility.
Section 2: Doing File Operations
What do these file operations do? What is the difference between the way we will copy a file in the section and the normal way? Well, the normal way is usually implemented using the API functions: CopyFile
, MoveFile
, DeleteFile
that belong to the File Storage API set, these functions will do the job, but the shell function can give you also the shell support for this file, meaning with the shell function, you can see the progress dialog when you do a copy operation, you can have the deleted files moved to the recycle bin. You can make simple undo of your operations. And you get as a bonus all the nice dialogs that appear when you do these operations through the explorer.
So, how it is done? Using the SHFileOperation
API function, this operation gets a struct
that contains all the info the operation needs, including the source and destination, special flags and more. The C# declaration of this function is:
[DllImport("shell32.dll" , CharSet = CharSet.Unicode)]
public static extern Int32 SHFileOperation(
ref SHFILEOPSTRUCT lpFileOp);
As you can see from the function declaration, it expects a structure called SHFILEOPSTRUCT
, here is its definition:
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct SHFILEOPSTRUCT
{
public IntPtr hwnd;
public UInt32 wFunc;
public IntPtr pFrom;
public IntPtr pTo;
public UInt16 fFlags;
public Int32 fAnyOperationsAborted;
public IntPtr hNameMappings;
[MarshalAs(UnmanagedType.LPWStr)]
public String lpszProgressTitle;
}
So, what do we have here? First, we have the hwnd
, a handle to the owner window, as in many Shell API functions, the shell many times will present User Interface dialogs to get some input, so you need to specify which window will be the owner of these dialogs.
Next we have the wFunc
value, this value sets which operation we are interested in, the options are Copy
, Move
& Delete
. In fact, there is one more option, Rename
, but it is very limited and you can get the same effect with the Move
operation, so I won't deal with it.
The pFrom
parameter is the parameter where you set the source files, and the pTo
parameter is where you set the destination files. Now you probably want to know how exactly you put a list of string
s into a single IntPtr
, Well this is NOT an array of string
s. The guys who designed these APIs used a special technique to store a list of string
s. They store it in one big string
, with a NULL
char between each string
, and double NULL
chars at the end of the string
, so let's say we want to copy to files: "c:\file.txt" and "c:\file2.txt", the requested pFrom
string should be: "c:\file.txt" + "\0" + "c:\file2.txt" + "\0\0"
. Yep, this is indeed strange but that is the reason why the parameter pFrom
cannot be marshaled as a normal string
, instead, I need to use Marshal.StringToHGlobalUni
which gives me a pointer to a copy of the string
on the native heap.
Next comes the fFlags
parameter. This parameter lets us control some aspects of the file operation, we can set flags to do Silent Mode (not displaying the progress dialog box), we can set that 'Yes to All' will be the response for any dialog box that will be displayed, we can avoid presenting the user error dialogs when they occur and more.
The rest of the flags are less interesting. fAnyOperationAborted
is where the ShFileOperation
function will give the result of whether the operation was aborted. hNameMappings
is rarely used and can help only if I'm interesting in the new names the user had to give during the operation. And finally, lpszProgressTitle
, if we set the flag FOF_SIMPLEPROGRESS
, the progress dialog box does not present the file names and is supposed to present the text of this parameter. When I tested this function, I couldn't get this parameter to show, it didn't show the file names with the SIMPLEPROGRESS
flag but it did not show the title parameter. What can I say, strange.
Now that we know how to do it, we will see the class I've made to wrap it up in a convenient way. The class is called ShellFileOperation
. It includes two enum
s to make life easier called FileOperations
and ShellFileOperationFlags
. The class has the following properties:
public FileOperations Operation;
public IntPtr OwnerWindow;
public ShellFileOperationFlags OperationFlags;
public String ProgressTitle;
public String[] SourceFiles;
public String[] DestFiles;
I think no explanation is needed here. Also, in the class is a small helper function that receives a string
array and returns a string
in the format I've mentioned earlier (double null
terminated string
), here is the code that does the job:
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;
}
And finally, the most important function in the class DoOperation
which creates a new struct
, sets his fields, gets a pointer on the heap memory for our special From
and To string
s, and calls the function SHFileOperation
with the struct
. Here it is:
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;
}
Yes, I know, I didn't say anything about the SHChangeNotify
. Although you are not obligated to use this function, it is recommended that after an application makes some changes to the file system, it will notify the changes to the shell, so it could update itself according to the changes. The SHChangeNotify
is the way you do it, this is an another shell API function, that its job is to notify the shell, nothing more. It receives the event that happened (there is an enum
), and two parameters, which depend on the event.
Here is an example of using this class, the following sample uses the shell copy to copy the files winmine.exe, freecell.exe and mshearts.exe from the system directory into the root directory. The first time the code will run, it shows a progress dialog box, the second time it also asks you if you want to override the previous files... just think about the code you would have to do to take care of all the possible failures while doing file IO operations.
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!");
On to the next section.
Section 3: Adding Files to the Recent Documents List
The recent document list is a special folder that you can find using the SHGetFolderLocation
or SHGetFolderPath
API which were introduced in Part 1. But if you want to make changes to this directory, you shouldn't make it directly because the changes will not update properly and won't be reflected in the start menu. Instead, to make the changes in the appropriate way, you should use the API function SHAddToRecentDocs
. So this is how this API looks like:
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
UInt32 uFlags,
IntPtr pv);
[DllImport("shell32.dll")]
public static extern void SHAddToRecentDocs(
UInt32 uFlags,
[MarshalAs(UnmanagedType.LPWStr)]
String pv);
No, it's not a mistake, there are two declarations to this API. The first parameter can be one of two values: SHARD_PIDL
or SHARD<CODE>_PATH
(well actually, there is an ANSI and Unicode versions of the second value). If you activate the function with the PIDL flag, it means that the second parameter pv
will hold the PIDL
of the file you want to add, But, if you put the PATH
flag in the flags
parameter, then it means that the second parameter is a string
, So because the second parameter can be sometimes an IntPtr
(when you use the PIDL
flag) and sometimes a string
(when you use the PATH
flag), I've written two declarations to this API.
Also, I've written a small wrapper class for this function. It includes the enum
for the possible flags and two static
methods, one for adding a new item to the document list and one to clear the list. Here is the code for the class:
public class ShellAddRecent
{
public enum ShellAddRecentDocs
{
SHARD_PIDL = 0x00000001,
SHARD_PATHA = 0x00000002,
SHARD_PATHW = 0x00000003
}
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);
}
}
Nothing to explain here. One thing to note is that if you want to clear the list, you just need to put a null
in the second parameter of the SHAddToRecentDocs
function. Here is how you use the class:
ShellLib.ShellAddRecent.AddToList(@"c:\windows\Rhododendron.bmp");
ShellLib.ShellAddRecent.ClearList();
Section 4: Managing Printers
Remember section 1? With the ShellExecute
stuff? and verbs? So if you want to print something that is printable, like a Word document or a bitmap, you just need to use the shell execute command with the verb "print
". That's pretty easy, I know. But there is some more printer stuff you can do with an API named SHInvokePrinterCommand
. Here is its C# declaration:
[DllImport("shell32.dll")]
public static extern Int32 SHInvokePrinterCommand(
IntPtr hwnd,
UInt32 uAction,
[MarshalAs(UnmanagedType.LPWStr)]
String lpBuf1,
[MarshalAs(UnmanagedType.LPWStr)]
String lpBuf2,
Int32 fModal);
The first parameter is where you put the owner window handle, the second parameter, uAction
is the type of command you want to perform, you can take one of the PrinterActions enum
values. lpBuf1
and lpBuf2
are two parameters which depend on the action being performed. The last parameter, fModal
sets whether the function should wait with its return to the end of the command or should it return immediately. Here are three samples of using this function:
Opening a printer:
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_OPEN,
"printer name comes here",
"",
1);
Showing the printer properties:
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_PROPERTIES,
"printer name comes here",
"",
1);
Printing a test page:
int Ret;
Ret = ShellLib.ShellApi.SHInvokePrinterCommand(
this.Handle,
(uint)ShellLib.ShellApi.PrinterActions.PRINTACTION_TESTPAGE,
"printer name comes here",
"",
1);
As you see, very simple. No wrapper class needed.
Extra Notes
Well, I thought about doing a section about Drag and Drop capabilities using the shell but I've discovered that C# supports all there is to it, so using the Shell API will be a total waste of time.
This finalizes the part of USING the Shell. The next article will be about extending the shell which lets you do very interesting things. Note that I'm skipping some MSDN article which talks about "extending" the shell using only registry manipulations. If you want to know how to do that, you should visit the following MSDN link (there is no programming involved), Shell Basics: Extending the Shell.
Update (09.01.2010)
I turns out I had a bug in the definition of SHFILEOPSTRUCT
structure which prevented some of the mentioned features to work properly.
The code was updated as follows: ShellNameMapping.cs:
ShellFileOperation.cs:
- Added
NameMappings
property and handling code to populate this property after the copy / move operation
ShellApi.cs:
- Added
SHNAMEMAPPINGSTRUCT
, SHNAMEMAPPINGINDEXSTRUCT
and SHFreeNameMappings()
to receive the NameMappings
- Added
SHFILEOPSTRUCT32
, SHFILEOPSTRUCT64
, SHFileOperation32()
, SHFileOperation64()
declarations - Changed
SHFileOperation()
to be a proxy that is calling the 32 or 64 version depending on the target machine - Added
GetMachineType()
The first three changes are described here.
The last three are crucial to run on 32 and 64 bit machines without crashes.
The new code provides an additional NameMappings
property on the ShellFileOperation
class that is populated after the copy / move operation. With this property, one can get the new names the user had to give during the file operation (e.g., if a target file already exists on Vista or Windows 7, he can copy it with a changed name). Also the code is now working on 32 and 64 bit machines.
Credit goes to Wolfram Bernhardt and Benjamin Schröter for finding and fixing the bug.
That's it!
Hope you like it. Don't forget to vote.
History
- 12th January, 2010: Initial version