Introduction
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
those features?
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.
Acknowledgements
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
available:
- Pressing
<Delete>
deletes a file to the Recycle Bin.
Pressing <Shift-Delete>
deletes a file directly, without
going to the Recycle Bin.
- Pressing
<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
dialog folder.
- 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
<Ctrl-V>
,
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
disabled.
Creating a Common Dialog
If you're using MFC, a File Open or File Save common dialog is created by
instantiating a CFileDialog
object and calling its
DoModal()
method. If you're using the Win32 API, the same dialog is
created by calling either GetOpenFileName()
or
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 SysListView32
is
actually the child of a parent SHELLDLL_DefView
control,
which is hidden behind the SysListView32
.
Although 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
the SysListView32
), subclass its window procedure to a new function
of your own design, then arrange to have your new function eat unwanted
WM_KEYDOWN
messages.
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 CDN_INITDONE
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
to prevent: <Delete>
, <Shift-Delete>
,
<Ctrl-X>
and <Ctrl-V>
. The function
contains code like this:
UINT CALLBACK NewKeybdHook( int nCode, WPARAM wParam, LPARAM lParam )
{
if (wParam == VK_DELETE)
{
return 1;
}
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
next.
(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
'click-on-a-highlighted-item'.
Clearing the 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
handles.
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) SysListView32
control.
(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 SysListView32
control's window procedure, then monitoring for WM_CONTEXTMENU
messages and discarding them:
LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg,
WPARAM wParam, LPARAM lParam )
{
switch (uiMsg)
{
case WM_CONTEXTMENU:
{
return 0;
}
break;
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 CDN_FOLDERCHANGE
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 WM_NOTIFY
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
monitor for 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 )
{
switch (uiMsg)
{
case WM_NOTIFY:
NMHDR* pnmhdr = reinterpret_cast< NMHDR * >( lParam );
if (pnmhdr->code == LVN_BEGINDRAG)
{
return 0;
}
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 CDN_FOLDERCHANGE
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.
Curiously, trapping 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
the COM IDropTarget
interface, see item #5 below) is doubtless the
same. In any case, defeating right-button drags is simple enough: just watch
for mouse WM_RBUTTONDOWN
and WM_RBUTTONUP
messages in
the SysListView32
control, and discard them. Since the dialog never
sees any right-clicks, a right-button drag is never initiated.
The code
to eat the mouse right-click messages ends up being just another
case
in the switch
statement already described earlier
for trapping the WM_CONTEXTMENU
messages:
LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg,
WPARAM wParam, LPARAM lParam )
{
switch (uiMsg)
{
case WM_CONTEXTMENU:
{
return 0;
}
break;
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
{
return 0;
}
break;
As before, you must remember to re-subclass the SysListView32
control each time a CDN_FOLDERCHANGE
notification is detected.
(5) Preventing File Drops Onto the Dialog
The LVN_BEGINDRAG
and WM_RBUTTONDOWN/WM_RBUTTONUP
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
files.
::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 CDN_FOLDERCHANGE
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
LVN_BEGINDRAG
and WM_RBUTTONDOWN/WM_RBUTTONUP
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.
Therefore RevokeDragDrop()
, while necessary, is not sufficient
to completely prevent unwanted file drags. You must also trap the
LVN_BEGINDRAG
and WM_RBUTTONDOWN/WM_RBUTTONUP
messages.
(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
a 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
initialized.
TBBUTTONINFO tbinfo;
tbinfo.cbSize = sizeof( TBBUTTONINFO );
tbinfo.dwMask = TBIF_STATE;
tbinfo.fsState = TBSTATE_HIDDEN | TBSTATE_INDETERMINATE;
::SendMessage( hwndToolBar, TB_SETBUTTONINFO,
(WPARAM)TB_BTN_NEWFOLDER,
(LPARAM)&tbinfo );
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 WM_NOTIFY
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 #define
FIND_TOOLBAR_BUTTON_IDS
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
class called 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 #include
FileDialog_ReadOnly.h
, create a CFileDialog_ReadOnly
object,
then call the object's DoModal()
method to actually run the dialog.
MFC veterans will recognize this is identical to using MFC's
CFileDialog
.
Win32 API vs. MFC
The 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
substituting 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
dialog.
For the sample project, I did use MFC to build a dummy dialog that acts as a
launchpad for debugging and testing the CFileDialog_ReadOnly
class.
But the dummy dialog should be understandable by non-MFC programmers; the only
code of 'interest' is within the OnButtonClickMe()
method of the
TestOpenFileDialogDlg.cpp file.
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
only *.CPL
, *.INI
, *.EXE
, or
*.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 <Delete>
or
<Shift-Delete>
, until you are confident the read-only dialog
works as advertised.
Summary
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
controls.
Additional Thanks
CodeProject members WREY, Paulo Rogério, and shue_20002000 contributed
valuable bug reports and helpful suggestions for improving the code submitted in my
original article.