Customizing the Common File Open Dialog
|
Setting the Hook
The hook is set as follows, in the OnInitDialog() handler:HookHandle = SetWindowsHookEx(WH_CALLWNDPROC, (HOOKPROC) Hooker, (HINSTANCE)NULL, (DWORD)GetCurrentThreadId());
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
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);
}
const int TB_BTN_UPONELEVEL = 40961; const int TB_BTN_NEWFOLDER = 40962;
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
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;
}
if(ParseForDelims(szEditBuff)) ::SetWindowText(x -> hwnd, "");
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
|

