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

Windows Explorer wildcard selection shell extension

0.00/5 (No votes)
28 Sep 2002 1  
A shell extension to allow you to select files based on a wildcard search

Introduction

If you have ever played with MS-DOS, you'll be familiar with wildcard patterns. For example, to get a listing of all text files in a particular directory, you would type dir *.txt. The Windows Explorer doesn't let you do that. I wrote Wildcard Select because I occasionally need to select all HTML files from a folder to perform some operation on them. In MS-DOS I could have specified the wildcard pattern *.html to select these files, but in Windows my only hope is to pick them by hand. You can, of course, switch to the Details View, sort on the Type column, and select all HTML files by dragging a box around them, but that's not particularly convenient. Plus, it doesn't work if you need to select files based on other criteria than their extension, for example all GIF files that begin with the prefix holiday. Using Wildcard Select you would type holiday*.gif and be done with it.

Wildcard Select is an Explorer shell extension, which means that it lives in Explorer's right-click menu. If you right-click on a file, folder, or the background of an Explorer window and choose the Select... item from the menu, the following dialog box pops up:

Wildcard Select screenshot

You can type the search pattern in the edit box. While you are typing, Wildcard Select tells Explorer to select all the files from the current window that match the pattern. If you press Cancel or erase the pattern, Wildcard Select restores the original selection, if any. If one or more files were selected at the time you invoked Wildcard Select, it will only try to match the items from that selection.

In this article, I'd like to explain how Wildcard Select works. It uses some COM and ATL, a little MFC, and messes around with Windows Explorer. I won't be showing you any code snippets (the article is long enough as it is already), but I will give a detailed explanation of the program's inner workings.

If you take a look at the source, you'll find that in some places I'm using STL's string class and in others I use C char arrays and functions such as strrchr(). From time to time, I also use MFC's CString. I prefer working with the STL classes, but sometimes it just makes more sense to work with the others. For example, if all the functions that use a particular string expect a char array, then it's a bit overkill to store that string as a CString or STL string, just for consistency's sake. I hope this doesn't make the source too confusing...

The boring stuff

You can read all you ever wanted to know about shell extensions in Michael Dunn's The Complete Idiot's Guide to Writing Shell Extensions series, so I am not going to go into much detail about writing the actual shell extension code. I'll just give you a brief recap here. A shell extension is a COM DLL. Instead of implementing all the COM logic itself, Wildcard Select uses just enough ATL to get by. The files that take care of this are Select.idl, CtxMenu.rgs, and Select.cpp.

The Select.idl file describes CtxMenu, which is our COM class, and ICtxMenu, its interface. CtxMenu doesn't really need an interface of its own, but IDL requires us to specify one. Likewise, we specify a typelib, but we don't use that either.

The CtxMenu.rgs file contains the registry entries for our shell extension. These entries tell Explorer that our shell extension must appear in the context menus for all files, folders, and the background of Explorer windows. It will not appear if you right-click on drive names or special folders such as the Control Panel.

The Select.cpp file contains several boring COM housekeeping functions. The DllRegisterServer() and DllUnregisterServer() functions register and unregister the DLL, using the information from the RGS file. Unlike a typical ATL application, we pass FALSE as the parameter to _Module.RegisterServer(). This only puts the GUID for our COM class in the registry, not the GUIDs for the ICtxMenu interface or the typelib. We don't use those, so there's no point cluttering up the registry.

Note that DllRegisterServer() and DllUnregisterServer() are not called by Explorer, but by the installer. If you downloaded the source files instead of the installer, you will have to register the DLL yourself using the command line tool regsvr32. If you compile the source from within Visual C++, then regsvr32 runs as part as the build process.

Because Wildcard Select uses MFC to build its GUI, Select.cpp also contains the CSelectApp class, which extends from MFC's CWinApp.

What did the user click on?

The interesting stuff happens in CtxMenu.h and CtxMenu.cpp. These files contain the declaration and implementation of the CtxMenu class that we saw in the IDL file. CtxMenu is the guts of Wildcard Select. Like all good context menu shell extensions, it implements the IShellExtInit and IContextMenu interfaces.

In short, this is what happens: When the user right-clicks inside an Explorer window, Explorer calls CtxMenu's Initialize() function. At this point, the context menu isn't visible yet. Explorer then calls the QueryContextMenu() function to add the Select... item to the menu, and shows it. When the user moves over this menu item, Explorer obtains a description from GetCommandString(). Finally, when the user picks the Select... item, Explorer calls InvokeCommand().

The only place where we can figure out whether the user clicked on a file or on the background of the window is in Initialize(). This distinction is important: if the user clicked on the background, then no files are selected and Wildcard Select must try to match all files against the wildcard pattern. On the other hand, if the user clicked on a file, there is a selection and Wildcard Select will only try to match the files from that selection.

The two functions that deal with this are ClickedOnBackground() and ClickedOnFileOrFolder(), respectively. In the former case, we set the allFiles flag to true. In the latter, we set it to false, and copy the names of the selected files into the selected list. In both cases, the folder field receives the name of the current folder.

Explorer's guts

Explorer calls the InvokeCommand() function when the user picks the Select... item from the context menu. This is where we pop up the dialog box and do the funky stuff. The funky stuff, of course, is selecting files in the Explorer window. But how do we do that? Well, fifteen seconds with Spy++ will tell you that Explorer simply uses a ListView control to display the filenames. And to select items in a ListView, we simply send it a message.

Conveniently, InvokeCommand() receives the HWND of the Explorer window. On my copies of Windows 95, 98, NT4, and XP, this window contains a child window with the class SHELLDLL_DefView, which in turn contains a child window with a class named SysListView32. This is the control we are after. On Windows Me and 2000, however, SHELLDLL_DefView contains not SysListView32, but Internet Explorer_Server, which in turn contains ATL Shell Embedding. This last window finally contains the SysListView32 that we need. Our FindListView() method takes the handle of the Explorer window, looks for SHELLDLL_DefView, and then simply recurses through all SHELLDLL_DefView's child windows until it finds the SysListView32 we're after.

Now that we've successfully obtained a handle to Explorer's ListView control, we show the dialog box. The dialog box is handled by our PatternDlg class, which is a fairly simple affair. Seen one, seen 'em all. We pass a this pointer to its constructor, because PatternDlg occasionally needs to call functions from CtxMenu. Specifically, it calls SelectFiles() whenever the user enters a character, and RestoreOriginalSelection() when the user erases the search pattern.

Selecting the files

Next up is the SelectFiles() function, which really does all the work. Instead of writing my own wildcard matching function, I decided to use the Win32 "find file" API functions, since they already know how to deal with wildcards. We need these functions anyway to read the contents of the current folder. So we'll simply combine the two by feeding the user's search pattern into FindFirstFile(). Then we'll loop through all the files it finds and tell Explorer's ListView to select the corresponding items.

But what are the corresponding items? As it turns out, we can't simply compare the filenames that we obtain from FindFirstFile() and FindNextFile() with the text from the list items, because what is shown in the ListView may not be the actual filename.

If the "Allow all uppercase filenames" option from Windows is disabled, then the case of the filenames may differ. For example, the corresponding list item for the file called AUTOEXEC.BAT is Autoexec.bat. If that was the only problem, we could have performed a case-insensitive compare, but alas, Windows also has a "hide file extensions for known file types" option. With that option enabled, a filename like cool.exe will show up as just cool, without the extension. Finally, the find functions will happily return hidden files, but your Explorer windows may be setup not to show them.

Surely, there must be a way for Windows itself to know which filename belongs to a particular list item? Well, I took a guess here, and assumed that the lParam member of a list item contains the PIDL to the file. (For more about PIDLs and stuff, see Mike Dunn's article on namespace extensions.) I was right - it must have been my lucky day ;-) We don't really need to do anything with that PIDL, except feed it to SHGetPathFromIDList(), which in turn gives us the path to the file that this list item represents. Nice, eh. Now we can compare that to the actual filename. All this magic happens in our GetListViewIndex() function. It returns the index of the file in the ListView.

Actually, SHGetPathFromIDList() gives us back the right filename, but the wrong path, always starting from the desktop (at least during my tests). This probably has something to do with relative and absolute PIDLs. (Again, see Mike's article.) However, I couldn't care less. We can simply ignore everything up to the filename; after all, the find functions don't include the whole path either, so we would have to strip it off anyway to do the comparison.

The SelectListViewItem() function, finally, takes the index that we obtained from GetListViewIndex(), and uses the ListView_SetItemState macro to send the message to the ListView control. Notice that we set not only the LVIS_SELECTED flag to select the item, but also LVIS_FOCUSED to give it the focus. If we don't, then the focus could end up on a file that is not selected, which is rather awkward if you're using the keyboard to interact with Explorer.

Other things

To provide as much feedback as possible, the selection is being made while the user types. Unfortunately, the ListView only shows the selected items if it has the focus. But because the user is typing in a dialog box, the ListView doesn't have the focus, and consequently doesn't show the selection until after the user closes the dialog box. At least, that's how things work on my Windows 98 machine. On XP, it does show the selection, but in light gray, not dark blue.

A quick glance at MSDN explains that the ListView has a special style called LVS_SHOWSELALWAYS that is responsible for this. Now it always shows the selection, even if the control does not have the focus. On older versions of Windows, Explorer doesn't set this style, even though its ListViews support it. Therefore, just before we show the dialog box, we set the LVS_SHOWSELALWAYS flag in the ListView. And would you believe it, now it does show the selection while the user is typing. Of course, we restore the original ListView style after the dialog box closes.

Being able to invoke Wildcard Select from the right-click menu on a folder's background is convenient, but as Mike Dunn remarks, there is a usability issue here. On all Windows versions prior to XP, the index passed into IContextMenu's QueryContextMenu() function is -1, which means that the Select... item is added at the bottom of the menu, below the Properties item. Out of habit, most users will expect Properties to be the last item on the menu, and will find it very confusing (and irritating) if we were to put Select... at the bottom. So we'll put it at the top instead. If index equals -1, then we pretend it is 0. Is this bad? I don't know. It seemed to work fine for me during testing... (By the way, the right-click in the window background only works with shell versions 4.71 or better.)

Because we compare the contents of the ListView with the actual filenames, shortcuts are a bit of a problem. Explorer doesn't show it, but the filenames of shortcuts actually end with the extension .lnk. If you want to match a shortcut, then your pattern should end with .lnk as well... (Fortunately for us, SHGetPathFromIDList() doesn't try to resolve the shortcut, or we'd be in real trouble.)

The SelectFiles() function isn't particularly smart. It contains several nested loops, and if the current folder contains lots of files (say, more than 2000), Wildcard Select can be pretty slow. I plan to speed it up somewhat in the future. Any suggestions are welcome!

Credits

Wildcard Select's dialog box uses two subclassed controls: an auto-completing combo box and a hyperlink. Both were loosely based on code written by Chris Maunder: Implementing an autocompleting Combobox and Hyperlink control. I changed the code around a bit to fit this project; if you want to re-use these classes, you're probably better off with Chris' original code instead. The shell extension code is based on the samples from Michael Dunn's Complete Idiot's Guide to Writing Shell Extensions articles.

What is new?

Version 1.0 gives an error message ("Cannot send message to Explorer") when you invoke it from something other than Explorer's list view. This happens, for example, if you right-click inside the tree view of the Explorer window. Now Wildcard Select cannot find the list view because this time the InvokeCommand() function is not given the HWND of Explorer's window, but the HWND of some other window.

I spend some time writing code that first finds the handle of the main Explorer window, instead of assuming that we already have it. But then I realized that invoking Wildcard Select from the tree view's right-click menu doesn't make much sense. Even though a folder may be selected in the tree view, that doesn't necessarily mean its contents are visible on the right. And it doesn't make much sense selecting files if you can't see them.

The simplest solution to this problem turned out to be: don't add the Select... item to the context menu unless it is invoked from Explorer's list view. We do this in QueryContextMenu. First, we get the HWND of the active window with GetFocus. As it turns out, that is the HWND of the list view, so that's easy. Then we make sure that the window's class is called SysListView32. If it's not, we simply bail out and don't add our menu item.

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