Click here to Skip to main content
11,579,662 members (74,055 online)
Click here to Skip to main content

File Rating - a practical example of shell extension

, 27 Dec 2004 106.4K 2K 91
Rate this:
Please Sign up or sign in to vote.
A shell extension that provides a new Rating column for folders, that allows to sort files by user interest

Introduction

This article presents a practical example of a Shell Extension that I've recently implemented. I was showing travel photos to friends, and I wanted to show only the best photos in my folders. One possible solution was to use one of the great photo managers around but I wanted to work directly with the standard Explorer interface.

The idea is really simple, add a new Rating column to the Folder View of Explorer under Windows XP. This new column can be used to sort the files (photos, MP3s ...) by the user defined rating and it works with the Photo Preview of Explorer, so I can show first the best photos. The side effect of this approach is that it is possible to sort the results of a Search by the Rating thus obtaining the best photos in a folder tree.

This is an example of the extended Explorer view:

Sample image

and these are the new entries in the context menu:

Sample image

Table Of Contents

  1. Shell Extensions - Overview, Debugging, Registration
  2. Implementation
  3. Improvements
  4. Conclusions
  5. References

Shell Extensions

The Windows Shell has a powerful extension mechanism that gives the possibility to provide new functionalities and interaction capabilities to the Shell. The implementation of these extensions in C++ and COM was quite difficult, but a lot has changed with the introduction of .NET. (In the References section, I've reported some links.)

This is a schema that explains the possible Shell Extensions that can be created and what is used by this article.

Debugging

The debugging of Shell Extension with .NET is the same as that of COM (see this). It's just necessary to specify the Explorer.exe as the executable for the debugging session and set to 1 the DesktopProcess DWORD value of the key.

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer

This value tells the Explorer to create a separate process for each window and one for the Taskbar, easing the debugging of the extension.

Registration

The registration process has an extension type specific part and the general part. The general part is mostly performed by the regasm.exe utility (invoked by VS.NET) that addresses all the COM-.NET connection. If the extension is intended to be installed on non-administrator accounts, it's important to register it under the Approved set of extensions:

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved

It's sufficient to add a new string value to this key with the name as the curly braced GUID of the class and an optional description. To inspect the installed extension, there is the Autoruns utility by SysInternals.

The extension specific registration can be performed directly by managed code using two special attributes ComRegisterFunctionAttribute and ComUnregisterFunctionAttribute. The following snippet of code presents how to implement that for registering this extension.

#region Registration
[System.Runtime.InteropServices.ComRegisterFunctionAttribute()]
static void RegisterServer(String str1)
{
    try
    {
        string keyname = "File Rating";
        string guid = "{"+typeof(RatingColumnHandler).GUID.ToString()+"}";
        RegistryKey rk,rk2;
        rk = Registry.ClassesRoot.OpenSubKey(@"*\shellex\ContextMenuHandlers", true);

        ...
    }
    catch(Exception e)
    {
    }
}

[System.Runtime.InteropServices.ComUnregisterFunctionAttribute()]
static void UnregisterServer(String str1)
{
    try
    {
        string keyname = "File Rating";
        string guid = "{"+typeof(RatingColumnHandler).GUID.ToString()+"}";
        RegistryKey rk;
        rk = Registry.ClassesRoot.OpenSubKey(@"*\shellex\ContextMenuHandlers", true);
        rk.DeleteSubKey(keyname, false);
        ...
    }
    catch(Exception e)
    {
    }
}
#endregion

Implementation

The File Rating extension uses a Column Handler to add a new column to the Detailed view of Explorer, the column is registered for in the folders ("Folder/shellex/ColumnHandlers" in the registry's Classes root) and implemented through the interface IColumnProvider. The values of the column can be used for sorting the files, also in the Preview mode of the Explorer.

I've decided to store the rating information of each folder into a .rating file where, in each line, there is the rating, a tab, and then the file name. This is the simplest solution, but there are some issues that I'll discuss in the future work. This file is cached by the Column Handler, and each time, it can be checked against changes using a timestamp.

The user can change the ratings by editing directly the .rating file or better by using a new entry in the Context Menu associated with every file ("*/shellex/ContextMenuHandlers" in the registry's Classes root). Two interfaces, IShellExtInit and IContextMenu are required for the correct implementation of this extension.

IShellExtInit

The method Initialize of this interface is invoked when the extension is going to be invoked on a new folder, and it gives the occasion for obtaining the folder and the files involved in the operation. This operation is carried on by using some Clipboard and Drag&Drop relative functions, and it could be nicely wrapped in a helper class. The list of the files is saved during the initialization phase and used later during the menu construction and invocation (for changing the menu depending on the selection).

IContextMenu

This interface is used to obtain the effective menu for the selection of files involved in the operation (QueryContextMenu) and to process the option selected by the user (InvokeCommand). Optionally, it can provide to the Shell, help information (GetCommandString).

The QueryContextMenu answers to the question of the layout of the menu, and it requires some Win32 menu construction. Each entry is associated to an identifier that is passed to the InvokeCommand when the user selects the menu entry. In this case, the menu is not dependent on the selection and it uses a submenu to group the options:

int IContextMenu.QueryContextMenu(HMenu hMenu, int iMenu, 
                 int idCmdFirst, int idCmdLast, CMF uFlags)
{
    int id = 1;
    if ( (uFlags & (CMF.CMF_VERBSONLY|CMF.CMF_DEFAULTONLY|CMF.CMF_NOVERBS)) == 0 || 
        (uFlags & CMF.CMF_EXPLORE) != 0)
    {
        HMenu submenu = ShellLib.Helpers.CreatePopupMenu();
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING|MFMENU.MF_ENABLED, 
                           new IntPtr(idCmdFirst + id++), "Rating ++");
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING|MFMENU.MF_ENABLED, 
                           new IntPtr(idCmdFirst + id++), "Rating --");
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING|MFMENU.MF_ENABLED, 
                           new IntPtr(idCmdFirst + id++), "Zero Rating");
        Helpers.AppendMenu(submenu, MFMENU.MF_STRING|MFMENU.MF_ENABLED, 
                           new IntPtr(idCmdFirst + id++), "About");
        Helpers.InsertMenu(hMenu, 5, 
                           MFMENU.MF_BYPOSITION|MFMENU.MF_POPUP|MFMENU.MF_ENABLED, 
                           submenu.handle, "File Rating");
    }
    return id;
}

Improvements

There are some issues about this extension, but they don't really prevent its usage.

  1. In the case of read only folders, the .rating file solution is not applicable, and a centralized solution is better, but it would increase the complexity of this extension.
  2. When a file is renamed or moved, the rating information is lost; this could be addressed by using a FileWatcher on the folder, but it has the side effect.
  3. The solution of an additional hidden file in a folder is not nice, because there are too many. An alternative solution that also solves problem #2 is to use the NTFS extended properties. This is a feature of NTFS 5.0 that gives the opportunity to associate properties to file like Title, Category, and in this case, the Rating. In general, the file rating information could be stored inside the metadata information of a Metadata based file system.
  4. When the user changes the Rating by the context menu, the Explorer view is not updated, and it's required to do a refresh command. (Can anyone suggest me how to do this?)
  5. The rating information could be computed in some automatic way, by tracking file usage or other information, like it's performed by the latest Windows Media Player.

Conclusions

This short article presents here a practical example of shell extension that can be used to rate files like photos or audio files. I hope that it will be useful to someone.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Emanuele Ruffaldi
Software Developer (Senior) Scuola Superiore S.Anna
Italy Italy
Assistant Professor in Applied Mechanics working in Virtual Reality, Robotics and having fun with Programming

You may also be interested in...

Comments and Discussions

 
QuestionDo we need a separate dll?
Goran _3-Jul-08 6:02
memberGoran _3-Jul-08 6:02 
[quote]
/// Column Handler for all files - adds an Rating hash of the file as a new column
///
/// Needs to be registered with RegAsm.exe and installed in the gac with GacUtil.exe
[/quote]

Why do we need to add this dll to GAC? I gues we need to use regAsm in order dll to be available to COM, but why GAC?

Also, is there no way we can make this classes to be part of the exe file, and not in a separate dll?

All my questions all related only to IShellExtInit and IContextMenu, since I would like only to add my menu item to context menu.

Thanks,
Goran
QuestionFor Folders to?
termal18-Jun-08 1:11
membertermal18-Jun-08 1:11 
Hi,
how to modify to works for folders to?

thanks
termal
QuestionCan't set breakpoint?
termal13-Jun-08 4:57
membertermal13-Jun-08 4:57 
Hello,
i have a little problem with the breakpoints, i want do debug this project,and the explorer starts immediate, execution jumps over my breakpoints,where is the entry point and where is the path defined:
C:\Documents and Settings\pit\Documenti\Immagini\Foto\2004\Atene 2004\ema
i except something like:
CString csString = "C:\Documents and Settings\pit\Documenti\Immagini\Foto\2004\Atene 2004\ema"
but i cant find anything like this!
sorry about but my question i cant solve them! Frown | :(

thanks for any help!
termal
Generalvista includes native star rating handlers in explorer
umeca7422-Apr-08 23:24
memberumeca7422-Apr-08 23:24 
eg see http://blogs.msdn.com/benkaras/Default.aspx?p=2[^]

PKEY_Rating is defined for windows XP too but i haven't seen any handler for any file type, so your code is still useful for backward compatibility

---
http://www.zabkat.com
GeneralDo not write in-process shell extensions in managed code
SteveKing20-Dec-06 2:16
memberSteveKing20-Dec-06 2:16 
tells you Raymond Chen.
GeneralRe: Do not write in-process shell extensions in managed code
Emanuele Ruffaldi20-Dec-06 2:37
memberEmanuele Ruffaldi20-Dec-06 2:37 

What a terrible mistake. Thanks!!!



GeneralRe: Do not write in-process shell extensions in managed code
AndyCLon3-Apr-08 1:35
memberAndyCLon3-Apr-08 1:35 
I agree, writing C++ code is easy. Getting it to compile seems to be the problem. The samples for shell extentions have caused me terrible trouble over the years and Visual Studio always seemed to be fighting with the Platform SDK. Dead | X|

So I was thinking of implementing some .net versions but I see why that also would be a bad idea.

Conclusion: Don't write in-process shell extensions. Frown | :(

It means that property pages, icons handlers and columns are not going to be available but I can install simple menus to launch forms to show the relavent information and provide interaction with the file. Sigh | :sigh:
GeneralRe: Do not write in-process shell extensions in managed code
Goran _3-Jul-08 5:57
memberGoran _3-Jul-08 5:57 
Does that mean this code is not safe to use?

Thanks,
Goran
QuestionHow do i provide two diffrent context menu basen on which extentions the files has.
thomaxz.tc14-Dec-06 9:57
memberthomaxz.tc14-Dec-06 9:57 
I like to provide a contemxnenu for a file, and another for all other files and directories.

how do i provide two diffrent context menu bassed on the file extentions.
QuestionRe: How do i provide two diffrent context menu basen on which extentions the files has.
thomaxz.tc14-Dec-06 21:21
memberthomaxz.tc14-Dec-06 21:21 
I found this site http://www.theserverside.net/tt/articles/showarticle.tss?id=ShellExtensions
which telling , how to provide diffrent menus based on, file context,
sow ithink i can reprogrammet the processbatchresulat to provide tru for one file type or false for all other.

but when i compilling the source cor, and register it witch regasm.exe, and copy it into gac with gacutil.exe, and restarting the explorer.exe from the processlist, i don't get ay menu.

what do i wrong?


GeneralTwo questions
afaz22-Aug-06 1:11
memberafaz22-Aug-06 1:11 
Firstly I'm thankfull for this good article...It's so long time that I need to add my own shell extension.I have two questions :
1)How to add extension on folder right click context mune?
2)How to add bitmap to created extension?
I hope that you will help with these questions.Thank you and good luck.Smile | :)

Nothing is impossible

Questionproblem with nvu ?
Ivi221-Aug-06 0:51
memberIvi221-Aug-06 0:51 
Hi
When i click the preview webpage button in nvu it does not work when the shell extension is registered. It jumps in the shell extension but it should
start the default browser.

http://www.nvu.com/
GeneralUnicode + Centralized database
OCedHrt17-Mar-06 13:35
memberOCedHrt17-Mar-06 13:35 
I recently wrote a shell extension using C# for hashing files and storing that hash in a centralized database. If you're interested, I could send you my code for reference. Because the files are hashed, they can be moved without losing their rating Smile | :)

The shelllib.cs used by your code and the MD5 column hasher loses unicode. It uses
[DllImport("shell32")]
internal static extern uint DragQueryFile(uint hDrop,uint iFile, StringBuilder buffer, int cch);

which defaults to the ANSI version of the function call. A preprocessor define for UNICODE would need to be set for it to link to the unicode version. However, even then the StringBuilder buffer breaks the wide-char array that it returns. After experimentation I found that unicode could be maintained with the folowing instead:
[DllImport("shell32")]
internal static extern uint DragQueryFileW(uint hDrop,uint iFile, Byte[] buffer, int cch);

After retrieving the byte array, it can be converted to a unicode string with UnicodeEncoding.
QuestionIcon and Status text
wakake26-Nov-05 10:37
memberwakake26-Nov-05 10:37 
Is there anyone that knows how you can get and icon next to the text and also get some text in the statusbar when you hover the item? Smile | :)
AnswerRe: Icon and Status text
wakake27-Nov-05 3:54
memberwakake27-Nov-05 3:54 
I fount out how to get the icon (bitmap) to show. The bitmap needs to be 13x13 pixel.

Add this to ShellLib.cs
After
[DllImport("user32")]
internal static extern bool InsertMenu(HMenu hmenu, int position, MFMENU uflags, IntPtr uIDNewItemOrSubmenu, string text);

Add this
[DllImport("user32")]
internal static extern bool SetMenuItemBitmaps(HMenu hmenu, int position, MFMENU uflags, IntPtr hBitmapUnchecked, IntPtr hBitmapChecked);


In the RatingColumnHandler.cs
After
uint m_hDrop = 0;
Add this
protected Bitmap bitmap = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Tesla.Shell.Resources.Image1.bmp"));
You can also set the bitmap in the "IShellExtInit.Initialize" section where you can catch exceptions if there is any Smile | :)

After
Helpers.InsertMenu(hMenu, 5, MFMENU.MF_BYPOSITION|MFMENU.MF_POPUP|MFMENU.MF_ENABLED, submenu.handle, "File Rating");
Add this
Helpers.SetMenuItemBitmaps(hMenu, iMenu, MFMENU.MF_BYPOSITION, bitmap.GetHbitmap(), IntPtr.Zero);
You can replace IntPtr.Zero with another bitmap that will be displayed when the item is checked

Link to MSDN[^]
GeneralChaning the background colour of a row
TimothyP23-Aug-05 23:23
memberTimothyP23-Aug-05 23:23 
Hi,

Is is possible to change the background colour of a row in the detail view of windows explorer? So that based on the rating it would have a different colour.

thnx.
GeneralNew version
Emanuele Ruffaldi19-Jul-05 23:46
memberEmanuele Ruffaldi19-Jul-05 23:46 

I've added some features to the rating application and dedicated a small website:

here

The new version allows to modify the file names through the concep of tag:

e.g.

P120230.jpg

with tags becomes

P120230 - Home - Family.jpg

GeneralSolution for improvement 4
panto27-Jun-05 22:13
memberpanto27-Jun-05 22:13 
You can use SHChangeNotify to refresh exploreBlush | :O
Generalsame bug like in my extensions
stax07111-May-05 10:53
memberstax07111-May-05 10:53 
Hi,
I'm currently examining other extensions because of this nasty bag. Everytime the "File" popup menu item of the main menu pops up a new popup is added. Does it happen on your system too?

Explorer -> main menu -> "File" popup menu

Evertime this popup a new menu item is added.

Regards,
stax
GeneralSimpler IShellExtInit Implementation
GenericMoniker9-Apr-05 2:35
memberGenericMoniker9-Apr-05 2:35 
I've posted a simpler implementation of IShellExtInit.Initialize at http://esmithy.net/articles/2005-04-08.html[^], if that's helpful.
GeneralRe: Simpler IShellExtInit Implementation
neo260077715-May-05 7:18
memberneo260077715-May-05 7:18 
Your code didn't work for me. Was there some other changes you made also?
GeneralRe: Simpler IShellExtInit Implementation
GenericMoniker16-May-05 4:17
memberGenericMoniker16-May-05 4:17 
That was the essence of it. Could you be more specific about what didn't work? Did the returned array not include the correct selected files? Thanks.
GeneralInstalling on non dev machine
Torsten Dicke20-Mar-05 9:30
memberTorsten Dicke20-Mar-05 9:30 
is there a How to .. to install dll on an non dev Computer ... tanks

GeneralRe: Installing on non dev machine
faken ame25-Oct-08 21:05
memberfaken ame25-Oct-08 21:05 
I'd like to know as well
GeneralFolder information - pidlFolder == 0
_Aaron_15-Mar-05 9:16
member_Aaron_15-Mar-05 9:16 
Nice article!

I've got my own context menu up and going, and everything is working out except for one detail: I need to get the path to the folder that the user is right clicking in. It's not good enough for me to just grab the path to the clicked file because the menu will be shown on any right click in the Explorer window, even if no files are selected.

Calling SHGetPathFromIDList() on the pidlFolder paramater should return the path to the folder the user has clicked on, but the pidlFolder pointer coming into the Initialize function is always 0...

I've checked this in the project referanced at 'the server side', as well another extension handling article at informit.com, but in all cases the pidlFolder is 0, and the call to SHGetPathFromIDList() returns an empty string.

Anyone have any ideas? Is there some way to get the target folder out of the IDataObj sent to Initialize()? I'm not hung up on how it happens, but I really need to get the path to the folder the user has clicked on... Confused | :confused:
AnswerRe: Folder information - pidlFolder == 0
afaz22-Aug-06 12:04
memberafaz22-Aug-06 12:04 
Hi palSmile | :)
I dont know did you get your answer to this question... anyway Im going to answer you.May you dont believe but you've got everything to get folder's path...In the last list of IShellExtInit.Initialize function we have:
m_hDrop = medium.hGlobal;
you can use this parameter by invoking DragQueryFile function.This function will return Shell object path which maybe file(s) or folder(s).I mean it will work in both cases! try this :
uint nselected = Helpers.DragQueryFile(m_hDrop, 0xffffffff, null, 0);
if (nselected == 1)
{
StringBuilder sb = new StringBuilder(1024);
DragQueryFile(m_hDrop, 0, sb, sb.Capacity + 1);
string m_file_or_folder_Name = sb.ToString();
}

it will work if user select one item you can use else to grab all items.
Good luckCool | :cool:

Nothing is impossible

GeneralRe: Folder information - pidlFolder == 0 [modified]
Cristian Amarie1-May-08 19:45
memberCristian Amarie1-May-08 19:45 
Unfortunately, if you right click a link item, it will give you the location of the real file and not the one where you right-clicked. You cannot rely in this case on DragQueryFile to extract the file(s) name(s) and get the parent folder from there.

Consider you make a shortcut to notepad on desktop.
When you right-click the shortcut, you expect that the folder where command was invoked to be
C:\Documents and Settings\<username>\Desktop

instead you will get (using DragQueryFile, get the C:\...\Desktop\notepad.lnk, and PathRemoveFileSpec to get the folder)
C:\Windows

because DragQueryFile will return you C:\Windows\notepad.exe and not the location of lnkfile.

You could register the shell extension for lnkfile in registry, but this will cause the context menu called once for lnkfile and once for AllFilesystemObjects (which most of general-purpose shell context extension are registered under...)


Nuclear launch detected
modified on Sunday, May 4, 2008 2:44 AM

GeneralShellLib.cs - GetCommandString
TormentoR-18-Feb-05 12:08
memberTormentoR-18-Feb-05 12:08 
I'm using the ShellLib.cs but the IContextMenu.GetCommandString() function doesn't seem to be working right.
I set a nice new String to commandString when GCS.HELPTEXT is asked for, but the helptext is never displayed.
Been google'n for hours now, something is wrong i don't know what, tried a lot!
GeneralRe: ShellLib.cs - GetCommandString
Eli Brody11-Nov-05 8:22
memberEli Brody11-Nov-05 8:22 
Yes, I suffered this problem as well. Luckily, I eventually discovered that the problem was in the way the buffer was interpreted as a StringBuilder. It would work, but only up to 16 characters.

I changed the declaration to:

[PreserveSig()]
void GetCommandString(
int idcmd,
uint uflags,
int reserved,
IntPtr commandString,
int cch);

Of course, now you have to put the string into the space pointed to by the IntPtr. In order to do that, you must once again DllImport:

public static extern IntPtr lstrcpy(
[Out] IntPtr lpString1,
string lpString2);

Now you're ready to go. In your implementation of GetCommandString, simply call lstrcpy:

lstrcpy(commandString, "Poot slap, poot slap.");

Enjoy.

Return to the Megastructure
http://www.megastructure.org/
GeneralNice
RussKie28-Dec-04 11:09
memberRussKie28-Dec-04 11:09 
good work!
Wink | ;)
GeneralInteresting but
Paul Watson28-Dec-04 0:28
staffPaul Watson28-Dec-04 0:28 
It is an interesting idea and thanks for coding it up but I would still like to see some code in your article and the explanation of it. I look forward to future updates.

regards,
Paul Watson
South Africa

The Code Project

Pope Pius II said
"The only prescription is more cowbell. "

GeneralRe: Interesting but
Emanuele Ruffaldi28-Dec-04 1:46
memberEmanuele Ruffaldi28-Dec-04 1:46 

I'm working on that,
thanks for the interest!
GeneralBroken Image link.
Mr.Prakash27-Dec-04 22:56
memberMr.Prakash27-Dec-04 22:56 
Need to be fixed.


-prakash
GeneralRe: Broken Image link.
Emanuele Ruffaldi27-Dec-04 23:20
memberEmanuele Ruffaldi27-Dec-04 23:20 
Sorry, I'm still editing the article

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150603.1 | Last Updated 28 Dec 2004
Article Copyright 2004 by Emanuele Ruffaldi
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid