Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5872228
  • 博文数量: 671
  • 博客积分: 10010
  • 博客等级: 上将
  • 技术积分: 7310
  • 用 户 组: 普通用户
  • 注册时间: 2006-07-14 09:56
文章分类

全部博文(671)

文章存档

2011年(1)

2010年(2)

2009年(24)

2008年(271)

2007年(319)

2006年(54)

我的朋友

分类: C/C++

2007-10-15 10:08:05

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

  1. Pressing deletes a file to the Recycle Bin. Pressing deletes a file directly, without going to the Recycle Bin.
  2. Pressing 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.
  3. allows a file to be Cut from its current location and pasted somewhere else.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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 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: , , and . The function contains code like this:

// 'NewKeybdHook' is the custom keyboard hook we installed.

UINT CALLBACK NewKeybdHook( int nCode, WPARAM wParam, LPARAM lParam )
{
    // wParam is the key code.

    if (wParam == VK_DELETE)    // user pressed the Delete key?
    {
    return 1;       // eat it: don't let dialog box see it
    }

In principle the same approach could also be used to trap (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 and left-mouse 'click-on-a-highlighted-item'.

Clearing the LVS_EDITLABELS flag is simple:

// 'hwndListView' is a handle to the SysListView32 control.

DWORD dwStyle = GetWindowStyle( hwndListView );  // get original style flags

// Remove the LVS_EDITLABELS style.

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

// 'NewListViewWndProc' is the subclassed SysListView32 window procedure.

LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg, 
    WPARAM wParam, LPARAM lParam )
{
    switch (uiMsg)
    {
        case WM_CONTEXTMENU:    // trying to create a context menu?
        {
            return 0;       // eat this message, prevent context menu
        }

        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.

// 'NewShellDefWndProc' is the subclassed SHELLDLL_DefView window procedure.

LRESULT CALLBACK NewShellDefWndProc( HWND hwnd, UINT uiMsg, 
    WPARAM wParam, LPARAM lParam )
{
    switch (uiMsg)
    {
        case WM_NOTIFY:     // notification message

            NMHDR* pnmhdr = reinterpret_cast< NMHDR * >( lParam );

            if (pnmhdr->code == LVN_BEGINDRAG)      // user trying to drag?
            {
                return 0;   // eat this message, don't allow drag
            }

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:

// 'NewListViewWndProc' is the subclassed SysListView32 window procedure.

LRESULT CALLBACK NewListViewWndProc( HWND hwnd, UINT uiMsg, 
    WPARAM wParam, LPARAM lParam )
{
    switch (uiMsg)
    {
        case WM_CONTEXTMENU:    // trying to create a context menu?
        {
            return 0;       // eat this message, prevent context menu
        }

        break;

        case WM_RBUTTONDOWN:    // detected any mouse right-click message?
        case WM_RBUTTONUP:      
        {
            return 0;   // eat it: no right-mouse dragging permitted
        }

        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.

// 'hwndListView' is a handle to the SysListView32 control.

::RevokeDragDrop( hwndListView );    // remove drag-n-drop registration

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.

// 'hwndToolBar' is a handle to the ToolbarWindow32 control.
// 'TB_BTN_NEWFOLDER' is the ID for the "New Folder" button.

TBBUTTONINFO tbinfo;

tbinfo.cbSize = sizeof( TBBUTTONINFO );
tbinfo.dwMask = TBIF_STATE;
tbinfo.fsState = TBSTATE_HIDDEN | TBSTATE_INDETERMINATE;

::SendMessage( hwndToolBar, TB_SETBUTTONINFO, 
    (WPARAM)TB_BTN_NEWFOLDER,   // which button
    (LPARAM)&tbinfo );          // new button state

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 or , 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.

阅读(1325) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~