Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ShellPilot - how to create an extensible Shell extension

0.00/5 (No votes)
23 Mar 2006 1  
This article describes how to develop a universal Shell context menu extension, extensible by user-defined commands written in any language.

Introduction

I'm working at Two Pilots as a developer, and since I have many different tasks to do always, I constantly create small utilities which help me speed-up my work. I like C++ very much, because it's the most powerful language I've ever seen, but I also like Python, C#, PHP, and even VBScript. What I also like is the Windows Shell - I use different OSs, but this GUI is the most habitual for me - that's because I think Gnome and KDE are very similar and mimic it's behavior.

The problem

Anyway, the Windows Shell can be extended only using compiled languages, because you have to implement several interfaces - IShellExtInit, IContextMenu, and so on. I don't like that. If I can write some utility in 15 minutes using Python and want to add it to the Shell as an extension, I have to rewrite it in C++ and debug it under Explorer. That's not an easy job - I think that's because we have relatively few good extensions. So, to correct this situation, I decided to create an easy-to-use Shell extension interface for all languages. If you'll read this article up to the end, you'll see that this is very easy, and I hope this small piece of code will be useful for you.

How to solve it?

It's very easy, and if you have a basic knowledge about Shell extensions, you can go further. If not, please read at least this article by Michael Dunn.

First of all, we have to know about what we get from the Shell when an extension is invoked, what kind of information is important for us etc. I think a file or a files list is to be selected by the user before invoking a context menu. Do you agree? So, we'll use a files list from the Shell. That's what we need.

OK, we have a files list and want to pass it to our utility - what do you think can we use to start a program and pass some parameters to it? Yes, of course - a command line. So, we have a files list which should be passed to some utility using a command line, that's clear. The best way to do this is to take a bat file as a base command, which in turn will invoke the user-defined commands. This is the main idea - here is a little illustration:

So, Let's get ready to rumble!� :-)

Design

To write some code right away, we need to design it first to avoid any superfluous work. We need to create a submenu to append to the Shell context menu. How do we create a menu that is easily modifiable? It should be created at runtime using some information. Now where do we get the information - XML, or any other file? I think that the best way to incorporate all features of our extension is to use the file system, and not some special file format. Since the file system has a tree-like structure, it's perfectly suitable for our needs. So, our menu will be dynamically created from some directory. Now, how must it be organized? Take a look at the picture - I think if we have such a directory tree:

our menu should look something like this:

E.g.: the root directory is used as a main menu item. Subdirectories without a bat file will be used as popup items. Directories with a bat file will be used as command items. As you can see from the picture, command items can have an icon. So, a command item directory should contain a bat file and may contain icons which will be loaded by a command item from an *.ico file, if it's present.

Implementation

We'll use C++ to write the base Shell extension, so here is the class relation diagram:

The first class which I want to describe is CMenuItemBase. It is designed as an interface. Let's see the classes that are derived from this interface?

We have two types of menu items - commands and submenus. The class which represents command items is CMenuCommandItem, and the class which represents a submenu is CMenuCommandsItem.

The class which manages all menu items and their IDs is CMenuItemsManager. Pointers to menu items are stored in a std::map where the key is the item ID and the value is the pointer to CMenuItemBase. A map is very handy in this situation, as we can get an item by its ID. We need CMenuItemBase to use pointers from derived classes in the items manager. By using the GetType() method of the CMenuItemBase, we can identify which class is under the pointer.

How are items are loaded from the file system? CMenuCommandsItem has a method CollectItems() which is called in the constructor. It recursively walks through the directory tree and creates class instances for each directory. So, to load all trees, we need only one thing - create the root item. All descendant items will be loaded automatically because of recursion. Very easy.

When all items are loaded from the file system, it's time to build the menu.

To create submenus, we'll use the method described here, but, as mentioned by grigri, we have to deal with item IDs. To use dynamic menu items in InvokeCommand, we should know the item identifier, but since Windows provides very limited sets of IDs, we can't use them directly (one ID for each item) - we need to leave some free IDs for other developers. So, I think we should use the dwItemData member of the MENUITEMINFO structure to identify each item. To track selected items using dwItemData, we need to implement the command items as an ownerdraw, store the item ID from dwItemData when it is drawn with the ODS_SELECTED flag, and use it in InvokeCommand.

After some research, I've found that the Shell doesn't handle popup submenu items with same IDs in the context menu - it means that each popup item should have a unique ID. This way, we have to use 2*n +1 IDs for all the context submenus. Because the first ID is always used for the root item and each popup submenu requires a unique ID, all the command items at the same level will take the subsequent IDs. In this case, the IDs usage is very economical, and Windows will be able to handle all items properly.

How are the item IDs assigned? Both CMenuCommandItem and CMenuCommandsItem, at creation time, call the constructor of the base class, which calls the CMenuItemsManager method, AddItem. The items manager assigns the ID for the item and stores the pointer in its map.

How it works?

First of all, CMenuItemsManager is created. It loads the path to the root directory from the registry. Then, inside the Initialize extension, it collects the file paths to the vector. Then, inside the QueryContextMenu, the root menu is loaded, and using method Build, the main popup menu is created. The DrawMenuItem extension stores the currently selected command item ID into a special variable. When InvokeCommand is called, this variable is used to get the item and to get the path to the item directory. The extension creates a simple text file inside the item's directory and fills it by the file paths from the vector - each line is one path. Then, the extension uses ShellExecute to call a bat file command with a files list file as a parameter. Then the bat file can invoke any program and pass the files list as a parameter. That's all - very easy! Isn't it?

About the code

As you can see, there is no code inside the article. Why? Because it's better to download the project and examine the code in DevStudio. The code is very well commented, so you can use the article as an explanation of the code when you cannot understand what it does or are unable to understand the comments.

I think the article explains the main idea. And I don't like two miles long articles :-)

Inside the archive, you'll find the full source code of the extension. Also, there is a registry_key.reg file, which points to the default location of the root directory - here, I've used C:\ShellPilot commands. Also, there is a "ShellPilot Commands" directory, which is used to perform tests and to take screenshots. So, to quickly test the extension, build the DLL, copy "ShellPilot Commands" to the C:\, and execute the registry_key.reg file. You'll get the same results as on the screenshot.

Which OSs are supported?

I think all Windows versions are supported with Shell 4.71 and higher. But, as you can see in the article header, I didn't include NT4 and Windows 2000. Why? Because I don't have them installed and can't tell you exactly that this code works well under NT4 and Windows 2000. So, if you have these versions, please test and leave a comment, or drop me a line, and I'll update article.

License

I've decided to choose GPL for this code. Why? Because I want to always use this code freely, even if some great guy will improve it and add some great features (I hope for it) :-)

Anyway, it concerns only the main extension, but all programs which are called by it can have other licenses. So, if you have some great utility, which you've created, you can send me the link, and I'll place it here.

That's all!

P.S.: Please don't beat me too much - this is my first article on CP :-)

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