Everyone is familiar with the standard 'File Open' and 'File Save' common
dialogs. Microsoft has done a great job of providing advanced functionality for
these controls. Each of them is essentially a miniature version of Windows
Explorer: in addition to presenting a list of files, the user can drag files
within the list, create new folders, rename files, drag-n-drop new files into
the dialog from outside, and cut, paste or delete files. These operations are
all available right in the middle of Opening or Saving the current file.
But what if you don't want to offer the user that much freedom? The common
dialogs offer a lot of powerful features, but is there any way of limiting
In one of our company projects we needed a way to limit a common dialog's
functionality to providing a strictly read-only view of files in a
folder. This article presents the solution we came up with.
Much of the code presented here was derived from code written by Dino
Esposito in an article entitled "The Logo and Beyond: Solutions for
Writing Great Windows 2000-based Apps". His original article, including
downloadable source code, is available in the August 1999 issue of
MSJ, accessible on Microsoft's web site.
Some of the code was also taken from the Code Project article "Customizing
the Windows Common File Open Dialog", written by S h a n x. His
original article, including downloadable source code, is available here on the
CodeProject web site. S h a n x presents a number of interesting techniques for
customizing common dialogs in general.
If you'd like an introduction on how to customize common dialogs, I recommend
reading the MSDN documentation on the Microsoft MSDN web site. Search for
"Customizing Common Dialog Boxes" to uncover useful articles.
Definition of a "Read-Only" View
We first consider what a "read-only" view of files in a folder involves. This
seems like a simple question, but as I discovered, there are subtleties. Several
times while writing the code I thought I was finished, but a colleague would
come along and ask "What if the user did this?" and point out another
loophole. (I think I've got them all covered now :-)
Within a standard 'File Open' common dialog, the following operations are
<Delete> deletes a file to the Recycle Bin.
<Shift-Delete> deletes a file directly, without
going to the Recycle Bin.
<F2> allows editing of a file's name. Clicking
with the (left) mouse button on a filename that is already highlighted, also
allows editing of the file's name.
<Ctrl-X> allows a file to be Cut from its current
location and pasted somewhere else.
<Ctrl-V> allows a file to be Pasted into the dialog
from another location. This allows new files to be added to the dialog folder,
and also overwrites (clobbers) any existing files of the same name already
present in the folder.
- A mouse right-click within the dialog brings up a context menu which
offers Delete, Rename, Cut, and Paste, among other things. The context menu
also offers a "New…" submenu, which allows creation of new objects within the
- Files within the dialog box can be dragged and dropped into a different
folder, thus moving the file to a new location. The new location can be
another folder within the dialog, or some drop target outside the dialog.
Note that both left-mouse-button and right-mouse-button drags are possible.
- The dialog itself is a drop target: it accepts files from an outside
source being dropped onto it. This is similar to
allowing files to be added to the dialog folder and clobbering any existing
files of the same name.
- The "New Folder" icon (available in the Toolbar at the top of the dialog)
allows a folder to be created within the dialog.
For a read-only file dialog, each of the above has to be prevented or
Creating a Common Dialog
If you're using MFC, a File Open or File Save common dialog is created by
CFileDialog object and calling its
DoModal() method. If you're using the Win32 API, the same dialog is
created by calling either
GetSaveFileName(), as appropriate. In either case the dialog's
OPENFILENAME data member offers a way of hooking the window
procedure associated with the dialog. This allows us to tap into the dialog's
message stream, providing an entry into customizing the dialog's behavior.
Structure of a 'File Open' Common Dialog
Like all dialogs, a File Open dialog consists of an overall Parent dialog
window which contains various child windows as controls. The diagram below
illustrates those elements whose behavior has to be altered to make the dialog
read-only. Their window class names and parent-child relationships can be
discovered by using a spy utility such as Spy++ (included with Visual Studio),
or the useful freeware WinDowse window dowsing tool.
Each of the items shown has to be hooked or subclassed (in the Windows sense)
in some way, so as to intercept and defeat its 'undesirable' behaviors.
One point of probable confusion is the role played by the
SHELLDLL_DefView control, shown by the dashed line in the diagram.
The window that actually displays the files and folders in the dialog is the
SysListView32 (aka 'ListView'). It has a number of set-able styles
that determine how the files appear; for example, whether they appear as "Large
Icons" or as a "List" or whatever. However the
actually the child of a parent
which is hidden behind the
SysListView32 control determines how the files are
displayed, many of the actual file operations are handled by the parent
SHELLDLL_DefView control. This is important to understand, because
it means that some of the behaviors we want to intercept are handled by the
SysListView32, and some are handled by the
SHELLDLL_DefView. The two windows have different functionalities,
and we have to consider both when it comes to detecting which one handles what.
Defeating the Undesired Behaviors
(1) Disabling Specific Keystrokes
To a seasoned Windows programmer, it might seem simple to trap and discard
keystrokes you don't want: just get a handle to the appropriate window (probably
SysListView32), subclass its window procedure to a new function
of your own design, then arrange to have your new function eat unwanted
Unfortunately this doesn't work. The problem is that when the common dialog
is instantiated, it installs a keyboard hook. This means it sees keyboard
messages before they are passed to the dialog window procedure.
Subclassing the procedure is therefore of no use: by the time your function sees
them, the keystrokes have already been acted on — the file has been deleted, or
renamed, or whatever.
To get around this we have to install our own keyboard hook once the dialog
has finished initializing. The dialog sends a
notification message once initialization is complete; we install our keyboard
hook at that time. Thereafter, it is our own keyboard function that gets to
intercept keystrokes before they are passed on to the original hook (and thence
on to the dialog procedure).
In the keyboard hook function, we can trap and discard the keystrokes we want
<Ctrl-V>. The function
contains code like this:
UINT CALLBACK NewKeybdHook( int nCode, WPARAM wParam, LPARAM lParam )
if (wParam == VK_DELETE)
In principle the same approach could also be used to trap
<F2> (edit), but there's an easier way to disable filename
editing: change the window style of the
SysListView32, as described
(2) Disabling Filename Editing
Disabling filename edits turns out to be easy once you know how. Editing of
filenames in a ListView control is enabled or disabled simply by setting the
appropriate bit in the window's style member. The flag of interest is
LVS_EDITLABELS. By clearing this bit, filename editing is
automatically disabled for both
<F2> and left-mouse
LVS_EDITLABELS flag is simple:
DWORD dwStyle = GetWindowStyle( hwndListView );
::SetWindowLong( hwndListView, GWL_STYLE, dwStyle & ~LVS_EDITLABELS );
The only subtlety here is that the ListView loses the style settings
whenever the user navigates to a different folder. Why? Because whenever a
different folder is chosen, the current
SysListView32 is destroyed
and a new one is created. This can be verified using a spy tool such as WinDowse and watching the window
But this is not really a problem. The dialog sends a
CDN_FOLDERCHANGE notification message once the user has navigated
to a different folder. By monitoring for this notification, we know when we have
to adjust the window style of the (new)
(3) Disabling the Context Menu
The items shown in a mouse right-click context menu are unpredictable: they
depend on whether specific software (such as WinZip or Norton Anti-Virus) is
installed on the system, on security settings (user permissions), and the
version of the operating system itself. Therefore, while it's possible in theory
to disable only specific items on the context menu, in practice doing so quickly
becomes a nightmare.
Easier and far more reliable is to simply disable the context menu entirely.
This is accomplished by subclassing the
control's window procedure, then monitoring for
messages and discarding them:
LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg,
WPARAM wParam, LPARAM lParam )
The only subtlety here is the same as mentioned earlier: subclassing is lost
whenever the user navigates to a different folder. You must re-subclass the
SysListView32 control each time a
notification is detected.
(4) Preventing File Dragging Within the Dialog
This is an example of the
SysListView32 child control working in
concert with its parent
SHELLDLL_DefView. It turns out the
controls support dragging either with the left mouse button or the
right mouse button. We therefore have to trap both types of activity if we
wish to disable file drags in the dialog.
Disabling Mouse Left-Button Drags
When a file drag is started within the
SysListView32 by holding
down the left mouse button, the control sends a
message to its parent
SHELLDLL_DefView with an
LVN_BEGINDRAG notification code. It's the
SHELLDLL_DefView that actually handles the subsequent file drag
operation within the ListView.
By subclassing the
SHELLDLL_DefView's window procedure, we can
WM_NOTIFY messages and discard any that have an
LVN_BEGINDRAG notification code. The
SHELLDLL_DefView never sees
the notification, so a file drag operation is never initiated. Result: mouse left-button file
dragging within the dialog ListView control is effectively disabled.
LRESULT CALLBACK NewShellDefWndProc( HWND hwnd, UINT uiMsg,
WPARAM wParam, LPARAM lParam )
NMHDR* pnmhdr = reinterpret_cast< NMHDR * >( lParam );
if (pnmhdr->code == LVN_BEGINDRAG)
Subclassing of the
SHELLDLL_DefView is lost whenever the user
navigates to a different folder because the control gets destroyed and
recreated, just as the
SysListView32 does. You must re-subclass the
SHELLDLL_DefView each time a
notification is detected.
Disabling Mouse Right-Button Drags
Under normal conditions, using the right mouse button to drag a file
within the dialog results in a context menu popping up when the file is dropped.
Right-dragging a file out of the dialog also gives a context menu when the file
is dropped into a different window. Either way, the user has an option to Move
the file to a new location. This behavior has to be disabled.
LVN_BEGINDRAG notifications turns out to
have no effect on disabling right-button drags. It appears right-drags
are handled by a separate mechanism, though the underlying implementation (via
IDropTarget interface, see item #5 below) is doubtless the
same. In any case, defeating right-button drags is simple enough: just watch
WM_RBUTTONUP messages in
SysListView32 control, and discard them. Since the dialog never
sees any right-clicks, a right-button drag is never initiated.
to eat the mouse right-click messages ends up being just another
case in the
switch statement already described earlier
for trapping the
LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg,
WPARAM wParam, LPARAM lParam )
As before, you must remember to re-subclass the
control each time a
CDN_FOLDERCHANGE notification is detected.
(5) Preventing File Drops Onto the Dialog
tricks described above only apply to drags
originating within the dialog itself. To prevent files from being dropped onto
the dialog from outside, the operating system has to be told "don't drop any
files here". The operating system is involved because file drag-n-drop across
processes requires interprocess communication (actually COM), which is handled
by the operating system.
A window registers itself as being capable of accepting dropped files by
notifying the operating system through a call to the COM
IDropTarget interface. Microsoft provides a function
RevokeDragDrop() to allow a window to un-register itself from being
a drop target. Therefore, simply calling
RevokeDragDrop() on the
SysListView32 window prevents the dialog from accepting dropped
::RevokeDragDrop( hwndListView );
The only subtlety here is the same as mentioned earlier: drag-n-drop gets
re-enabled whenever the user navigates to a different folder, because the
SysListView32 control gets recreated. You must therefore call
RevokeDragDrop() each time a
notification is detected.
Why Intercepting Both Drag-n-Drop Handlers is Necessary
The reader may wonder: if
RevokeDragDrop() is sufficient to
disable file drops onto the dialog, shouldn't it also work to disable
file drags starting within the dialog? After all, either you're disabling
drag-n-drop, or you're not. Drags originating within the dialog can't be so very
different from drags originating outside the dialog.
The answer is,
RevokeDragDrop() used by itself does work, sort
of. If you call
RevokeDragDrop() and don't implement the
code described above in step #4, you'll discover that
file drags originating within the dialog can't be dropped anywhere within the
dialog. Seems good.
But what happens if you drag files from inside the dialog to outside
of it? Now you're in trouble. You'll discover that the file can be moved out of
the dialog folder and dropped into another location. This happens because the
target window can ask the operating system (COM) to move the file rather than
just copy it. This is no good.
RevokeDragDrop(), while necessary, is not sufficient
to completely prevent unwanted file drags. You must also trap the
(6) Disabling the "New Folder" Toolbar Button
The toolbar that appears at the top of the dialog box is a
ToolbarWindow32 control which contains a set of toolbar buttons.
Each button has its own ID. As explained in the Code Project article by S h a n x,
once we know the ID of a button, removing the button is accomplished by sending
TB_SETBUTTONINFO message to the toolbar to set the button's state
to "hidden". This only has to be done once, after the dialog has been
tbinfo.cbSize = sizeof( TBBUTTONINFO );
tbinfo.dwMask = TBIF_STATE;
tbinfo.fsState = TBSTATE_HIDDEN | TBSTATE_INDETERMINATE;
::SendMessage( hwndToolBar, TB_SETBUTTONINFO,
The only challenge is how to find the ID's of the buttons in the
ToolbarWindow32. The easiest way to do this is to subclass the
ToolbarWindow32 control and monitor for
messages sent by the toolbar button(s) you're interested in. For example,
hovering the mouse over a button eventually causes tooltip help to pop up, at
which time a
WM_NOTIFY for that button is sent to the
ToolbarWindow32. Since a
WM_NOTIFY contains the ID of
the item it was sent from, the ID of the toolbar button can be found.
Discovering the button ID's in this way only has to be done one-time, in
debug mode. Thereafter you can remove the code used to detect them and just use
the ID's as hard-wired constants. This is a bit of a kluge, since there is no
guarantee that the ID's will be the same across all versions of Windows.
However, experiments show that the same ID's are used in Win98, Win2K, WinNT4,
and WinXP, so that covers most of the Windows versions currently in use.
For convenience, the source code contains a
option which you can turn on to enable detection
of toolbar button ID's, if you want to experiment.
Using the code
The functionality of the read-only File dialog is encapsulated in a small
CFileDialog_ReadOnly. The class is not rigorously
object-oriented since it has to provide a keyboard hook and several window
callback procedures, and by their nature these cannot be member functions.
However the class is easy to use: just
, create a
then call the object's
method to actually run the dialog.
MFC veterans will recognize this is identical to using MFC's
Win32 API vs. MFC
CFileDialog_ReadOnly class is coded almost entirely in
standard (non-MFC) C++, because I wanted to make it accessible to non-MFC
coders. However for parsing filenames and things I needed a string class, so I
broke down and ended up using MFC's
CString. No other MFC features
or functionality is used, so non-MFC programmers should have little difficulty
std::string (or any other string class they prefer)
for the few appearances of
CString that do occur in the code.
CFileDialog_ReadOnly uses the Win32 API to create the file common
For the sample project, I did use MFC to build a dummy dialog that acts as a
launchpad for debugging and testing the
But the dummy dialog should be understandable by non-MFC programmers; the only
code of 'interest' is within the
OnButtonClickMe() method of the
Running the Demo
Running the sample project launches the dummy dialog, which looks like this:
Clicking 'Click Me' launches a read-only File Open dialog box showing the
contents of the System directory on your machine. Note that the dialog displays
*.DLL files, by way of demonstrating how visible files can be
limited to those having a particular extension.
I use the System directory as an example because it's a directory that's
always available. This guarantees the demo will work on everybody's machine. But
this is a dangerous folder to run tests on! — I suggest navigating to a less
important folder before trying out
<Shift-Delete>, until you are confident the read-only dialog
works as advertised.
Creating a read-only version of the File Common Dialogs turns out to be
possible, though not especially simple. To defeat the unwanted behaviors, it's
necessary to install several different types of message hook at different points
within the dialog. Doing this without affecting the overall functionality of the
dialog requires understanding the responsibilities of each child control within
the dialog window.
The techniques examined here, while focused on the File dialogs, would
hopefully prove useful in customizing the behavior of other common dialogs and
CodeProject members WREY, Paulo Rogério, and shue_20002000 contributed
valuable bug reports and helpful suggestions for improving the code submitted in my
David Kotchan is IT manager of software development projects at Optima Communications Canada in Toronto.