Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5833563
  • 博文数量: 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:20:13

Download demo project - 15 Kb

Sample Image

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:
  1. Start up in a particular desired directory, specified by the client.
  2. Display only folders in that directory, not files.
  3. Hide the two buttons of the toolbar, so that the user cannot move up one level or create a new folder.
  4. 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.
  5. 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:
  1. Create a common file open dialog.
  2. Setup a hook that intercepts all messages that flow to the common file open dialog.
  3. Trap desired messages, for desired child windows-say, the toolbar or the combo box.
  4. 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 : // 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:
  1. LPARAM
  2. WPARAM
  3. Message
  4. 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:

Collapse
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 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) // 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 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 )
{
   // Clear selection, if any
   DWORD dwSel = GetSel();
   LOWORD(dwSel) == HIWORD(dwSel) ? NULL : SetWindowText(""); 
   CString strWindowText; GetWindowText(strWindowText);  
   if (nChar == '\\' || nChar == ':' || 
      (nChar ==   '.' && strWindowText.GetLength() < 1))
      return; // Don't pass on to base for processing 

   CEdit::OnChar(nChar, nRepCnt, nFlags); // Pass on to base for processing
}

The above logic takes care of the following:

  1. User cannot enter ':'
  2. User cannot enter '\'
  3. 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 '.'.
  4. 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.
  5. 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.
阅读(1392) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~