MSDN Magazine > Issues and Downloads > 2002 > January >  C++ Q&A: OpenDlg Fixes Preview Problems
From the January 2002 issue of MSDN Magazine
MSDN Magazine
OpenDlg Fixes Preview Problems
Paul DiLascia
Download the code for this article: CQA0201.exe (63KB)
Browse the code for this article at Code Center: OpenDlg

R
ecently I got an e-mail from a reader in Germany who noted that the comment in my code "Compiles with Visual C++® 6.0 for Windows® 98 and probably Windows 2000 too" is, and I quote, "not very funny." He then pointed out: "In a week, Windows XP will be the system of choice and you're still writing for Windows 98."
      Well, never one to ignore readers, and having achieved my goal of sitting out one entire version cycle, I can happily report that I have now finally upgraded my desktop to Windows XP, two weeks before the official ship date. I have also, likewise, upgraded my comment to fit: "Compiles with Visual C++ 6.0 for Windows XP and probably Windows 2000 too." So, just remember, don't anyone ever call me a Luddite!

Q My program overrides the CFileDialog box to add a preview pane so when the user clicks on one of our files, it shows what the file contains. However, during development one of our testers found that if you select a file, then select a directory (without changing to that directory), the preview pane still displays the file. I handle OnFileNameChange (CDN_SELCHANGE) and in that function I call CFileDialog::GetPathName to get the selected file name. But GetPathName returns the name of the file, even when a directory is selected. I tried using other notifications and functions like CDN_FOLDERCHANGE and GetFileName, but nothing seems to work. How can I find out whether the user has clicked on a file or a directory?
Mike Liss

Q I have a problem with the standard file open dialog. Is there any way to know whether the user clicked on a file or a folder when my program gets the CDN_SELCHANGE notification? It seems that the OPENFILENAME struct doesn't get updated when switching between a file and the folder. It only gets updated when switching between files.
Shail Arora

A Time again to visit the file open dialog. And what a pesky dialog it is, too! It does indeed seem that on Windows 2000 (for those of you still using it), when the user selects a file, then a folder, GetPathName (CDM_GETFILEPATH) returns the name of the file previously selected, not the folder. Well what do you expect—perfection? Remember, it's these little quirks that keep programmers in business. If it were easy, your grandmother would be doing this stuff!

Figure 1 Spy++
Figure 1 Spy++

      To fix the problem, you have to get at the list control inside the dialog. Many of the common dialogs have controls with documented IDs like stc1 for a static text control and lst1 for a listbox; these symbols are #defined in <dlgs.h>. You might think the list control would be lst1, but spelunking with Spy (see Figure 1) reveals that the list control is actually contained in another window of class, SHELLDLL_DefView. The SHELLDLL_DefView window has ID lst2; the list control inside it has child ID 1. So to get the list control, you can write:
// from your CFileDialog derivate
CListCtrl* plc = (CListCtrl*)GetParent()->
  GetDlgItem(lst2)->GetDlgItem(1);
      Remember, when you customize the open file dialog, your CFileDialog is actually a child of the real dialog, which explains why you must use GetParent. (For details see my article "Give Your Applications the Hot New Interface Look with Cool Menu Buttons" in the January 1998 issue of MSJ.) The cast to CListCtrl* works as per the usual MFC trick, because CListControl has neither data members nor virtual functions; it's a pure wrapper class. (Since GetDlgItem returns a pointer to a temporary CWnd, not a CListCtrl, downcasting would normally be a major booboo.)
      Once you have a pointer to the list control, you can do whatever you like—within reason. For example, to get the selected path name, call CListCtrl::GetItemText and append the result to the current open folder (GetFolderPath/CDM_GETFOLDERPATH). Once you have the path name, how do you tell if it's a file or the folder? Here's how:
#include <sys/stat.h>
// test whether pathname is a folder
static BOOL IsFolder(LPCTSTR pathname)
{
  struct stat st;
  return stat(pathname, &st)==0 
    && (st.st_mode & _S_IFDIR);
}
Note, however, that just because the path name supplied is not a folder, doesn't mean you can therefore conclude it's a file! The item might be some other shell object like My Network Places (Network Neighborhood) or My Computer.
      I wrote a program, OpenDlg, to demonstrate how to do a preview dialog (see Figure 2). OpenDlg lets you select multiple items; the preview pane shows the first few lines of a .txt file if only one is selected. OpenDlg also has a Debug window that lists the selected items with "(FOLDER)" next to the ones that are folders. Figure 3 shows OpenDlg in action.

Figure 3 OpenDlg in Action
Figure 3 OpenDlg in Action

      OpenDlg uses a nifty helper class, CFileDlgHelper. To use it, all you have to do is instantiate and call Init.
class CMyOpenDlg ... {
protected:
  CFileDlgHelper m_dlghelper;
};
BOOL CMyOpenDlg::OnInitDialog()
{
  m_dlghelper.Init(this)
•••
}
Once initialized, you can use CFileDlgHelper to get the list control as well as item names and the "is folder" property. For example:
CListCtrl* plc = m_dlghelper.GetListCtrl();
POSITION pos = plc->     
  GetFirstSelectedItemPosition();
while (pos) {
  int i = plc->GetNextSelectedItem(pos);
  if (fdh.IsItemFolder(i)) {
    // display name with "(FOLDER)"...
  } else {
    // don't
  }
}
      If the selected item is a folder, OpenDlg blanks the preview pane, thus fixing the preview problem that Mike asked about. Of course, if you people weren't so behind the times, using Windows 2000 instead of Windows XP, you wouldn't have this problem! In Windows XP, OnFileNameChange/CDN_SELCHANGE returns the correct path name for files and folders. But you can still use CFileDlgHelper to get the list control, item names, or other cool stuff described in the next question, and you still need IsFolder to check for folders.

Q I tried to customize your CMyOpenDlg class, which appeared in the January 1998 issue of MSJ, but have had little success in making the Press me! button do something useful. I use OFN_ALLOWMULTISELECT to allow multi-file selection and specify *.txt as one of the filters. I want to add a Select All button to select all .txt files.
      Also, I want to use ON_UPDATE_COMMAND_UI to disable this button if there are no .txt files in the current directory. I've tried handling WM_KICKIDLE to activate idle processing so my ON_UPDATE_COMMAND_UI gets called, but to no avail. And within the button handler, I don't know how to update the contents of the listbox to indicate they are selected.
Graham Pearson

A Well, here we go again! If you understood the discussion in the previous questions, you know how to solve the last part of your problem—selecting all the items in the list control. Just use CFileDlgHelper to get the list control, and select all items with a .txt extension.
void CMyOpenDlg::OnSelectAll()
{
  CListCtrl* plc = m_dlghelper.GetListCtrl();
  for (int i=0; i<plc->GetItemCount(); i++) {
    CString fn = plc->GetItemText(i,0);
    if (IsTextFileName(fn)) {
      plc->SetItemState(i,LVIS_SELECTED,
        LVIS_SELECTED);
    }
  }
  plc->SetFocus();
}
      In fact, my OpenDlg program has a Select All button that does just this. As for ON_UPDATE_COMMAND_UI, that's a little more work. You will recall (you will recall!) from MFC 102 that UI update normally happens when your main message loop goes into idle state—that is, when there are no messages in the queue. But dialogs are different; when you run a modal dialog, MFC starts another message loop. When there are no messages waiting, CWnd::DoModal sends your dialog a WM_KICKIDLE message. So the normal way to hook up your dialog for idle UI processing is like so:
LRESULT CMyDialog::OnKickIdle(WPARAM wp, LPARAM lp)
{
  UpdateDialogControls(this, TRUE);
  return 0;
}
      CWnd::UpdateDialogControls sends the magic CN_UPDATE_COMMAND_UI message to your dialog, triggering your ON_UPDATE_COMMAND_UI handlers. But alas, this scheme doesn't work for the file open dialog. Why not? Because the CFileDialog override for DoModal doesn't run a message loop the normal way. Instead, it calls ::GetOpenFileName (or ::GetSaveFileName). These API functions run their own message loops, and there's no way to get inside them to do idle processing. Or is there?
      Whenever a modal dialog goes idle waiting for a message to arrive, the dialog sends its owner a WM_ENTERIDLE message. Hey, this is just the ticket to do the UI update thing! There are only a few details to fret. First, Windows sends WM_ENTERIDLE to the dialog's owner—in this case, your main frame—so you have to trap the message there. Next, Windows continues to send WM_ENTERIDLE as long as the dialog remains idle, whereas you only need to call UpdateDialogControls once. No problem, you can do the usual set-a-flag thing. And when do you reset the flag? Whenever the UI state could potentially change, which is after the dialog gets a WM_COMMAND or WM_NOTIFY message. So you have to trap those messages too, in the dialog.
      Since all this is such a bother, I encapsulated it into a new class, CFileDialogHelper. Once you call CFileDialogHelper from your dialog's OnInitDialog, you don't have to worry your little brain about a thing; all your ON_UPDATE_COMMAND_UI handlers magically work. How does CFileDialogHelper do it? By using my ubiquitous CSubclassWnd class, the one that lets you subclass windows in the Windows sense, by installing a new window proc ahead of the old one. In fact, CFileDialogHelper uses two CSubclassWnds: one to trap WM_ENTERIDLE sent to the dialog's parent, and another to trap WM_COMMAND or WM_NOTIFY sent to the dialog itself. When the main window gets WM_ENTERIDLE, CFileDialogOwnerHook intercepts it and updates your dialog controls.
LRESULT CFileDialogOwnerHook::WindowProc(...)
{
  if (msg==WM_ENTERIDLE) {
    if (m_pHelper->m_bUpdateUI) {
      m_pDlg->UpdateDialogControls(m_pDlg, FALSE);
      m_pHelper->m_bUpdateUI=FALSE;
    }
  }
  return CSubclassWnd::WindowProc(msg, wp, lp);
}
And when the dialog gets WM_NOTIFY or WM_COMMAND, CFileDialogHook resets the flag.
LRESULT CFileDialogHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
  if (msg==WM_COMMAND || msg==WM_NOTIFY) {
    m_pHelper->m_bUpdateUI = TRUE;
  }
  return CSubclassWnd::WindowProc(msg, wp, lp);
}
What could be easier? Once you know the voodoo. Note that there are two CSubclassWnd-derived classes, CFileDialogOwnerHook and CFileDialogHook; one to hook the main frame and one for the dialog itself, both hidden inside CFileDlgHelper. With CFileDlgHelper, the UI update handler for Select All looks just like you'd expect:
void CMyOpenDlg::OnUpdateSelectAll(CCmdUI* pCmdUI)
{
  CFileDlgHelper& fdh = m_dlghelper;
  CListCtrl* plc = fdh.GetListCtrl();
  for (int i=0; i<plc->GetItemCount(); i++) {
    if (IsTextFileName(fdh.GetItemName(i))) {
      pCmdUI->Enable(TRUE);
      return;
    }
  }
  pCmdUI->Enable(FALSE);
}
It uses the same IsTextFileName in the first snippet for OnSelectAll. IsTextFileName looks for a name that ends in .txt. But does this really work?
      There is a problem, a fatal flaw and fly in the ointment of OpenDlg. If the user has customized Explorer to hide extensions for known file types (see Figure 4), then .txt doesn't appear in the list control. CFileDlgHelper::GetItemName returns foo instead of foo.txt. In fact, if extensions are hidden, foo.txt, foo.jpg, and foo.doc all appear as foo. (Go ahead and try it.) So how do you know if foo is really foo or foo.txt?

Figure 4 Hiding Extensions for Known File Types
Figure 4 Hiding Extensions for Known File Types

      I'm sure there's an answer to this question, but to find out what it is, you'll have to read next month's column. Same bat time... Same bat channel...

Send questions and comments for Paul to cppqa@microsoft.com.
Paul DiLascia is a freelance writer, consultant, and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++(Addison-Wesley, 1992). Paul's Web site is http://www.dilascia.com.

Page view tracker