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:
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.