Customizing the Common File Open Dialog


BOOL CALLBACK EnumChildProc(HWND hWnd, LPARAM lParam)
{
int id = ::GetDlgCtrlID(hWnd);
switch(id)
{
case LOOK_IN_COMBO : // Combo box on top of the dialog
::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:

a. LPARAM

b. WPARAM

c. Message

d. 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 ids 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 were interested in. For the list view controls 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 : // Magic message sent after all items are inserted
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;

} // end switch

HideToolbarBtns(hWndToolbar);

} // end if

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 toolbars
buttons by calling the helper function HideToolbarBtns() that passes
receives the toolbars handle. How did we come to have the handle of the
toolbar? The following code does this-it simply saves the toolbars handle
for later use:


if(strcmp(_strlwr(szClassName), “toolbarwindow32”) == 0)
{
if(!CCustomFileDlg::OnceOnly) // Save toolbar’s handle only once
{
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 toobars 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. For this, we need to trap the
Return key event, which happens when the user presses enter after keying in a
path inside the edit box. Here is the code:


if(strcmp(_strlwr(szClassName), “edit”) == 0)
{
switch(x -> message)
{
case EDIT_ENTER: // User presses Enter
::GetWindowText(x -> hwnd, szEditBuff, MAX_CHAR);

if(ParseForDelims(szEditBuff))

::SetWindowText(x -> hwnd, “”);

break;
} // end switch
} // end if

EDIT_ENTER takes a value of 14 and is defined in
the header file. This is the message that is generated when the user
presses return at the edit box in the common file open dialog. Since
we have the handle to the edit control as well, we get the text inside
the edit control, which is actually the new path the user enters. We
then call the helper function ParseForDelims() with the path string.
The function will return a BOOL, to indicate if the user had entered
either a or a : or a . any of the presence of which could mean
that the user typed in a different path or a wild card sequence. If this
is the case, the function returns TRUE, else returns FALSE. Here is
the code:


BOOL ParseForDelims(const TCHAR* szEditBuff)
{
for(int i = 0; i < (int)strlen(szEditBuff); ++i) if(szEditBuff[i] == '\' || szEditBuff[i] == ':' || szEditBuff[i] == '.') return TRUE; return FALSE; }

So, if the user did indeed enter a new path, we simply set the text back
inside the edit control to NULL:


if(ParseForDelims(szEditBuff))
::SetWindowText(x -> hwnd, “”);

This 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 clients responsibility
to free the memory associated with the list. Also note: 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.

Known Problems

While the program works fine for every other directory, it does not get rid
of folders in the Windows directory.

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.

Downloads

Download demo project – 48 Kb

More by Author

Must Read