Download demo project - 15 Kb
Abstract
This article describes how to go beyond the normal customization
associated with the Windows common file open dialog. The common
file open dialog is easily one of the most often customized entity
in the Windows world, but given the limited customization options
included with it, it usually isn't very intuitive as to how go
about customizing it the way you want. This article tells you how
to make it look and behave the way you want it to. Readers are
reminded that though the following ideas and concepts deal with
the common file open dialog, the techniques presented here can
easily be applied to any other common dialog as well.
Aim
Customizing the common file open dialog has the following goals:
- Start up in a particular desired directory, specified by the client.
- Display only folders in that directory, not files.
- Hide the two buttons of the toolbar, so that the user cannot move up one level or create a new folder.
- Make the combo box on the top of the dialog that shows the current folder read only, so that the user may not select another directory from here.
- Prevent the user from changing directory when user
types in different paths in the edit box at the bottom of the dialog. Includes
wild card characters.
Basic Architecture
Basically the following steps need to be taken, to achieve the above goals:
- Create a common file open dialog.
- Setup a hook that intercepts all messages that flow to the common file open dialog.
- Trap desired messages, for desired child windows-say, the toolbar or the combo box.
- Change the behavior by writing customization code.
In Detail
Creating the common file open dialog
Creating the common file open dialog is a simple process. However,
it should be sub-classed first. Sub-classing is necessary, because
we need access to the dialog's OnInitDialog() handler, which is
where we basically set up a hook. Towards this end, we have a class
CCustomFileDlg derived publicly from CFileDialog. The OnInitDialog()
function is overridden. Also, the destructor for the derived class
removes the hook that was set in the OnInitDialog() function. The
m_ofn structure is filled with relevant details, and DoModal() is
called to create and display the dialog.
Making Controls Read Only
In the dialog's OnInitDialog() handler, first, the combo box on the
top of the dialog is made read only. This is done by enumerating the
child windows, till we reach the combo box and its handle. The
EnumChildProc() function does this:
BOOL CALLBACK EnumChildProc(HWND hWnd, LPARAM lParam)
{
int id = ::GetDlgCtrlID(hWnd);
switch(id)
{
case LOOK_IN_COMBO :
::EnableWindow(hWnd, FALSE);
break;
}
return TRUE;
}
The id for the combo box is identified by LOOK_IN_COMBO, is 1137
and is defined in the header file.
Setting the Hook
The hook is set as follows, in the OnInitDialog() handler:
HookHandle = SetWindowsHookEx( WH_CALLWNDPROC,
(HOOKPROC) Hooker,
(HINSTANCE)NULL,
(DWORD)GetCurrentThreadId() );
The hook is of type WH_CALLWNDPROC. This means that the hook
gets all messages first, before the target window procedure(s)
gets it. Therefore, custom processing can be made at this point,
since we get a first crack at the message.
Inside the Hook Procedure
The hook procedure is passed a pointer to a CWPSTRUCT in
its LPARAM. This structure contains the following information,
about the message intended for a particular window:
- LPARAM
- WPARAM
- Message
- Window Handle
For our purposes, the window handle and the message are the
most important. Much processing will depend on these two
parameters.
The main reason we have this hook is that we cannot subclass
the controls on the dialog straight away, since some of the
controls, such as the list view control and toolbar do not even
exist in the common dialog template, which is present in the VC++
include directory. Also, their id's are not known-they are not
available in the template, which makes things much harder to work
with. Stranger still, there is a list box control with an id lst1
that seems to be in the template for no rhyme or reason. This is
hidden, and the list view control sits on top of it, when the
dialog shows up.
Two of the most important things when working with windows are
its handle and id. One major problem with the sub-classing approach
is as follows: In order to sub-class a control, the control must be
there first! The list view control on start up shows the folders and
files. We can now sub-class it, but then, the damage is already
done-it shows the folders and files-we need only the files! Ideally,
we need something that intercepts the list view control before
it initializes, so that we can remove the folders-and then, we can
let the list view control continue to display only the files. Using
a hook is most convenient for us since it circumvents certain problems
of traditional sub-classing. One of the main advantages of using a
WH_CALLBACK type hook is that before a control gets the message, we get
it first. Therefore, we can trap an intended message for the list
view control, and modify it by changing processing. Further, the
CWPSTRUCT pointer passed in to the hook via the LPARAM has vital
information that we can put to use-the window handle and message.
The hook is a catchall. In other words, all messages generated go to
the hook first. Since we want to select which windows we need to
modify, we must first identify the target window handle. This is done
by using the GetClassName() API on the window handle obtained via the
CWPSTRUCT. GetClassName() returns the class name as a string, of the
window we're interested in. For the list view control's handle,
GetClassName() returns "syslistview32" and for the toolbar, it returns
"toolbarwindow32". And for the last control we want to modify, the edit
control, it returns "edit".
The following code shows how to obtain the class name for a control
whose handle is identified in the CWPSTRUCT pointer:
CWPSTRUCT *x = (CWPSTRUCT*)lParam;
GetClassName(x -> hwnd, szClassName, MAX_CHAR);
Using the relevant class names, we do processing accordingly. For
example, if it is a "syslistview32" based control, do something, if it
is a "toolbarwindow32" based control, do something else, etc. This is achieved
by making simple calls to strcmp().
Customizing the List View Control
Here is the code:
if (strcmp(_strlwr(szClassName), "syslistview32") == 0)
{
switch(x->message)
{
case WM_NCPAINT :
case LAST_LISTVIEW_MSG :
{
int count = ListView_GetItemCount(x-> hwnd);
for(int i= 0; i < count; i++)
{
item.mask = LVIF_TEXT | LVIF_PARAM;
item.iItem = i;
item.iSubItem = 0;
item.pszText = szItemName;
item.cchTextMax = MAX_CHAR;
ListView_GetItem(x -> hwnd, &item);
if (GetFileAttributes(szItemName) & FILE_ATTRIBUTE_DIRECTORY)
ListView_DeleteItem(x -> hwnd, i);
break;
}
}
}
HideToolbarBtns(hWndToolbar);
}
A check is performed first using strcmp() to make sure it is the
list view control. If it is, we switch to the message part of the
CWPSTRUCT pointer (in this case, x). Two messages are trapped,
WM_NCPAINT needed so that folder items can be removed before the
list view control actually shows up and LAST_LISTVIEW_MSG (defined
in my header file) which is the last message the list view control
receives after displaying all items. The message has a value of
4146, which I figured out by studying the messages to the list
view control.
Since we now have a handle to the list view control, we can perform
ordinary list view control operations-in this case, we simply run
down the entire list of items, checking to see if any item has the
FILE_ATTRIBUTE_DIRECTORY attribute set-which would mean it is a
directory. If so, we delete it. Finally, we hide the toolbar's buttons
by calling the helper function HideToolbarBtns() that passes receives
the toolbar's handle. How did we come to have the handle of the
toolbar? The following code does this-it simply saves the toolbar's
handle for later use:
if (strcmp(_strlwr(szClassName), "toolbarwindow32") == 0)
{
if (!CCustomFileDlg::OnceOnly)
{
hWndToolbar = x -> hwnd;
++CCustomFileDlg::OnceOnly;
}
}
Hiding the Toolbar
Here is the code for HideToolbarBtns():
void HideToolbarBtns ( HWND hWndToolbar )
{
TBBUTTONINFO tbinfo;
tbinfo.cbSize = sizeof(TBBUTTONINFO);
tbinfo.dwMask = TBIF_STATE;
tbinfo.fsState = TBSTATE_HIDDEN | TBSTATE_INDETERMINATE;
::SendMessage(hWndToolbar,TB_SETBUTTONINFO,
(WPARAM)TB_BTN_UPONELEVEL,(LPARAM)&tbinfo);
::SendMessage(hWndToolbar,TB_SETBUTTONINFO,
(WPARAM)TB_BTN_NEWFOLDER,(LPARAM)&tbinfo);
}
The code simply sets the new button states for the toolbar buttons. The
hard part was figuring out the ids of the toobar's buttons. In this case,
we use TB_BTN_UPONELEVEL and TB_BTN_NEWFOLDER, which are the buttons
the user might click to go up one level and to create a new folder
respectively. Both these are defined in the header file as follows:
const int TB_BTN_UPONELEVEL = 40961;
const int TB_BTN_NEWFOLDER = 40962;
Again, these numbers come from a long time spent with the debugger figuring
out the messages send to window handles and experimenting with different
ids for the toolbar buttons.
Finally, we need to make sure that the user cannot change directories by
entering different paths in the edit box. There are two ways of doing
this: we can either trap the Return key event, which happens when the
user presses Return after keying in a path inside the edit box or
subclass the edit control on the dialog, so that characters that denote
a change of directory cannot be entered. Since this article is all about
subclassing, it should come as no surprise we choose the latter
approach! This first thing to do is to create your own derived edit
control class, as defined in myedit.h:
class CMyEdit : public CEdit
{
protected:
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
DECLARE_MESSAGE_MAP()
};
We have an OnChar handler in the class, since we're interested
in trapping Now create an object of type CMyEdit in the customfiledlg.cpp
file, and finally here's the implementation in myedit.cpp:
void CMyEdit::OnChar ( UINT nChar, UINT nRepCnt, UINT nFlags )
{
DWORD dwSel = GetSel();
LOWORD(dwSel) == HIWORD(dwSel) ? NULL : SetWindowText("");
CString strWindowText; GetWindowText(strWindowText);
if (nChar == '\\' || nChar == ':' ||
(nChar == '.' && strWindowText.GetLength() < 1))
return;
CEdit::OnChar(nChar, nRepCnt, nFlags);
}
The above logic takes care of the following:
- User cannot enter ':'
- User cannot enter '\'
- First character entered cannot be a '.' thereby not allowing users to type
in '..', which would go up one level. Characters other than the first character
can be a '.'.
- I found that when you select the text, and then you typed in
a '.' as the first character, it would be entered, so to avoid the case
wherein a selection exists, I first set the text in the control to NULL
(empty). The GetSel() API function is used for this purpose, as shown above.
- Wildcards are possible, since you can use the '*'.
At this point, you might be wondering why I never made the edit control
a member of my custom dialog class, and why I did not subclass that in my
dialog's OnInitDialog() and why I instead chose to create a member of the
edit control in the .cpp file and handle its subclassing in my hook
code. The reason: it doesn't work. I'd be more than happy to hear any
explanations from avid readers.
Now, we have an edit control on the file open dialog that effectively
prevents the user from navigating to different directories by typing
in those directory paths inside the edit control.
Miscellaneous
On a final note, the customized file open common dialog supports multiple
selections. All the selections made are stored and parsed into a CStringList
object, containing each user selected item which is fully
qualified, i.e., contains the full path. A pointer to this list is returned
to the client from the exported function, getFileNames(). It is the client's
responsibility to free the memory associated with the list. Also note that
when passing in the path as a parameter to getFileNames() the client should
make sure that the path that depicts the directory is always terminated by a
trailing '\' and not left open. In other words, "C:\\TEMP" is not acceptable,
and should be changed to "C:\\TEMP\\". The way I see it, the former represents
a file and the latter represents a directory. This method of ending directories
with "\\" clears the ambiguity.
Possible Enhancements
There is a shell interface called ICommDlgBrowser. This has a
method called IncludeObject that lets you filter specific items
in your common file dialogs. This should be looked into.