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


Comments

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Managing your company's financials is the backbone of your business and is vital to the long-term health and viability of your company. To continue applying the necessary financial rigor to support rapid growth, the accounting department needs the right tools to most efficiently do their job. Read this white paper to understand the 10 essentials of a complete financial management system and how the right solution can help you keep up with the rapidly changing business world.

  • Agile development principles have gone from something used only by cutting-edge teams to a mainstream approach used by teams large and small. If you're not using agile methods already though, or if you've only been exposed to agile on small projects here and there, you may wonder if agile can ever work in your environment. Read this eBook to learn the fundamentals of agile and how to increase the productivity of your software teams while enabling them to produce higher-quality solutions that better fulfill …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds