Click here to Skip to main content
15,887,450 members
Articles / Desktop Programming / MFC
Article

Multiple Selection in a File Dialog

Rate me:
Please Sign up or sign in to vote.
4.94/5 (49 votes)
23 Nov 2002CPOL4 min read 278.7K   5.1K   61   58
Shows how to do multiple file selection in a file dialog without having to worry about the size of the buffers

Introduction

When using the Windows FileOpen dialog with multiple selection do you ever wonder how much memory you have to allocate for the buffer. Is one kilobyte going to be enough? Or should you make it ten? How about one Megabyte just to be safe?

It seems that no matter what you choose, you are either going to waste a whole bunch of memory just to be safe, or your user is going to select a bunch of files only to find their selection didn't work because your buffer was too small.

Well no more. If you use the method I am about to describe your problems will be over.

How To

The first and most important thing is that this method will only work with the Explorer style file dialogs because we make use of various messages that the old windows 3.1 style file dialogs do not support.

When you are browsing through your file system using the file dialog, the dialog will send WM_NOTIFY messages to the OFNHook procedure. ( Note: In MFC the hook procedure is set up automatically when you use the CFileDialog class, look up OPENFILENAME in MSDN if you want more information on setting up the OFNHook procedure if you are not using MFC. ) We are interested in trapping the CDN_SELCHANGE notification message. The CDN_SELCHANGE notification message is sent whenever the selection changes in the list box that displays the currently open folder. In MFC you can handle this message by overriding the CFileDialog::OnFileNameChange() function.

Now, in our CDN_SELCHANGE handler, the first thing that is required is to check if the buffer that was allotted using the OPENFILENAME structure is large enough. If it is, then we do not have to bother duplicating the default behaviour. If however, the buffer is too small then we have to take matters into our own hands.

To figure out the required size of the buffer we send two messages to the file dialog. They are the CDM_GETFOLDERPATH and CDM_GETSPEC messages. The CDM_GETFOLDERPATH message will return the required size of a buffer needed to hold the currently selected folder path, and the CDM_GETSPEC will return the required size of the buffer needed to hold all the file names. Now, just add these two values together and if the sum is greater than the nMaxFile member of the OPENFILENAME structure then the supplied buffer is too small.

Now, in order to retrieve the file names from the file dialog, we will have to set up two buffers of our own. One for the folder path and an other for the files. Use the CDM_GETFOLDERPATH message to fill the folder buffer, and use the CDM_GETSPEC message to fill the files buffer. All the files in the files buffer will be enclosed between quotation marks, and separated by spaces. ( In other words, exactly as they are shown in the "File Name" edit box. ) At this point it is also advisable to set some sort of flag to let us know later that we have used our own buffers, and not the default one.

void CFECFileDialog::OnFileNameChange()
{
    TCHAR dummy_buffer;
    
    // Get the required size for the 'files' buffer
    UINT nfiles = CommDlg_OpenSave_GetSpec(GetParent()->m_hWnd, 
        &dummy_buffer, 1);

    // Get the required size for the 'folder' buffer
    UINT nfolder = CommDlg_OpenSave_GetFolderPath(GetParent()->m_hWnd, 
        &dummy_buffer, 1);

    // Check if lpstrFile and nMaxFile are large enough
    if (nfiles + nfolder > m_ofn.nMaxFile)
    {
        bParsed = FALSE;
        if (Files)
            delete[] Files;
        Files = new TCHAR[nfiles + 1];
        CommDlg_OpenSave_GetSpec(GetParent()->m_hWnd, Files, nfiles);

        if (Folder)
            delete[] Folder;
        Folder = new TCHAR[nfolder + 1];
        CommDlg_OpenSave_GetFolderPath(GetParent()->m_hWnd, Folder, 
            nfolder);
    }
    else if (Files)
    {
        delete[] Files;
        Files = NULL;
        delete[] Folder;
        Folder = NULL;
    }

    CFileDialog::OnFileNameChange();
}

Now, another thing we have to handle is when the user clicks the OK button. When the OK button is clicked, the file dialog will return IDOK if there were no errors, however, in our case there will be an error as the default buffer was too small, so the file dialog will return IDCANCEL. What we have to do now is check the error code using the CommDlgExtendedError() function and check if the error was FNERR_BUFFERTOOSMALL ( defined in cderr.h ). If that was the error, and our flag was set to tell us we have used our own buffer, then all is well with the world and we can get the file names from our own buffer.

int CFECFileDialog::DoModal()
{
    if (Files)
    {
        delete[] Files;
        Files = NULL;
        delete[] Folder;
        Folder = NULL;
    }

    int ret = CFileDialog::DoModal();

    if (ret == IDCANCEL)
    {
        DWORD err = CommDlgExtendedError();
        if (err == FNERR_BUFFERTOOSMALL/*0x3003*/ && Files)
            ret = IDOK;
    }
    return ret;
}

All that is left is to extract the names from the buffers. In the supplied demo, I have overridden the GetStartPosition() and GetNextPathName() CFileDialog member functions in order to make this easier.

Using the CFECFileDialog class

If you want to use the supplied class in your own code, just add the FECFileDialog.h and FECFileDialog.cpp files to your project, and use the CFECFileDialog class the same as you would use the CFileDialog class.

That's it. I hope some of you find this useful, because I really hate to waste your time and mine :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President
Canada Canada
Father of two, brother of two, child of two.
Spouse to one, uncle to many, friend to lots.
Farmer, carpenter, mechanic, electrician, but definitely not a plumber.
Likes walks with the wife, board games, card games, travel, and camping in the summer.
High school graduate, college drop-out.
Hobby programmer who knows C++ with MFC and the STL.
Has dabbled with BASIC, Pascal, Fortran, COBOL, C#, SQL, ASM, and HTML.
Realized long ago that programming is fun when there is nobody pressuring you with schedules and timelines.

Comments and Discussions

 
GeneralRe: GetOpenFileName() Error Pin
PJ Arends21-Aug-04 6:23
professionalPJ Arends21-Aug-04 6:23 
QuestionObviously ??? Pin
sps-itsec4621-Jun-04 8:16
sps-itsec4621-Jun-04 8:16 
GeneralBrilliant Pin
Rob Manderson12-Mar-04 23:39
protectorRob Manderson12-Mar-04 23:39 
GeneralIt's all been said... well done and thx! Pin
krutsch25-Jan-04 15:10
krutsch25-Jan-04 15:10 
GeneralAnother thank you Pin
clinth2-Jan-04 7:54
clinth2-Jan-04 7:54 
GeneralThanks very very much Pin
Orgen Kl1-Dec-03 1:12
Orgen Kl1-Dec-03 1:12 
Generalproblem with CDN_FILEOK ( OnFileNameOK ) Pin
petr1234520-Nov-03 12:04
petr1234520-Nov-03 12:04 
Generalcode enhancement & bug fix for single file name selected Pin
petr1234520-Nov-03 11:52
petr1234520-Nov-03 11:52 
One disadvantage that I find in the published code is the fixed class hierarchy. In case you already have a class derived from CFileDialog ( for instance the CFileDialogEx pblished in MSDN mag Aug.2000 http://msdn.microsoft.com/msdnmag/issues/0800/c/default.aspx and you want to add the multiselection fix to that, there is no place in the class hierarchy to insert CFECFileDialog - unless you (case by case) change one of the classes code to derive from the other.

A solution of that is making CFECFileDialog a template ( the code below). Two additional changes i made :
i/ for simplicity it is using just single buffer ( CByteArray m_buffer; ) instead of two buffers; the buffer delallocates itself automatically in CByteArray destructor.
ii/ in case there is just sinle file name selected, the original did not parse the buffer well. In such such situation there are no quotes in the buffer ( see below TFEFileDialog<TBaseDialog>::ParseFileNamesBuffer() ).


// FEFileDialog.h header file
//
#ifndef _INC_CDERR
#include <cderr.h> // for FNERR_BUFFERTOSMALL
#endif // _INC_CDERR


/* #define COMPILE_DLGEXT */

#ifdef COMPILE_DLGEXT
#include "FileDialogEx.h"
#endif // COMPILE_DLGEXT

#if !defined(AFX_FECFILEDIALOG_H__F15939B0_B05A_11D4_B625_B7559D96EF20__INCLUDED_)
#define AFX_FECFILEDIALOG_H__F15939B0_B05A_11D4_B625_B7559D96EF20__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define N_BUFFER_GROWBY (1024*sizeof(TCHAR))

template<class TBaseDialog> class TFEFileDialog : public TBaseDialog
{
protected:
CByteArray m_buffer;
UINT m_nFileNamesOffset; // the offset ( in bytes )
mutable BOOL m_bParsed;
// Being mutable, it is legal to change that data member
// from a const member function ( like GetStartPosition() ).

public:
/// the class constructor. Arguments are the same as in MFC CFileDialog.
TFEFileDialog(BOOL bOpenFileDialog,
LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter = NULL,
CWnd* pParentWnd = NULL);

/// The virtual destructor.
virtual ~TFEFileDialog();

/// Overwritten DoModal.
virtual int DoModal();

// Enumerating multiple file selections. Overloads the functuinality of CFileDialog.
POSITION GetStartPosition() const;
CString GetNextPathName(POSITION &pos) const;

protected:
LPCTSTR GetBufferPtr() const;
LPCTSTR GetFileNamesPtr() const;
void ParseFileNamesBuffer() const;
void FreeBuffer();
virtual void OnFileNameChange();
};

class CFECFileDialog : public TFEFileDialog<CFileDialog>
{
DECLARE_DYNAMIC(CFECFileDialog)

/// the class constructor. Arguments are the same as in MFC CFileDialog.
CFECFileDialog(BOOL bOpenFileDialog,
LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter = NULL,
CWnd* pParentWnd = NULL);
/// The virtual destructor.
virtual ~CFECFileDialog();

protected:
//{{AFX_MSG(CFECFileDialog)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

#ifdef COMPILE_DLGEXT

class CFECFileDialogEx : public TFEFileDialog<CFileDialogEx>
{
DECLARE_DYNAMIC(CFECFileDialogEx)

/// the class constructor. Arguments are the same as in MFC CFileDialog.
CFECFileDialogEx(BOOL bOpenFileDialog,
LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter = NULL,
CWnd* pParentWnd = NULL);
/// The virtual destructor.
virtual ~CFECFileDialogEx();

protected:
//{{AFX_MSG(CFECFileDialogEx)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};

#endif // COMPILE_DLGEXT


template<class TBaseDialog>
TFEFileDialog<TBaseDialog>::TFEFileDialog(
BOOL bOpenFileDialog, LPCTSTR lpszDefExt, LPCTSTR lpszFileName,
DWORD dwFlags, LPCTSTR lpszFilter, CWnd* pParentWnd) :
TBaseDialog(bOpenFileDialog, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd)
{
m_nFileNamesOffset = 0;
m_bParsed = FALSE;
}

template<class TBaseDialog>
TFEFileDialog<TBaseDialog>::~TFEFileDialog()
{
}

template<class TBaseDialog>
void TFEFileDialog<TBaseDialog>::FreeBuffer()
{
if (m_buffer.GetSize())
{
m_buffer.SetSize(0, N_BUFFER_GROWBY);
m_nFileNamesOffset = 0;
m_bParsed = FALSE;
}
else
{
ASSERT(m_nFileNamesOffset == 0);
ASSERT(m_bParsed == FALSE);
}
}

template<class TBaseDialog>
LPCTSTR TFEFileDialog<TBaseDialog>::GetBufferPtr() const
{
if (m_buffer.GetSize() > 0)
return (LPCTSTR)m_buffer.GetData();
else
return NULL;
}

template<class TBaseDialog>
LPCTSTR TFEFileDialog<TBaseDialog>::GetFileNamesPtr() const
{
ASSERT(m_buffer.GetSize() > 0);
return (LPCTSTR)( m_buffer.GetData() + m_nFileNamesOffset);
}

template<class TBaseDialog>
int TFEFileDialog<TBaseDialog>::DoModal()
{
int ret;

FreeBuffer();
ret = TBaseDialog::DoModal();
if (ret == IDCANCEL)
{
DWORD dwerr = CommDlgExtendedError();
if (dwerr == FNERR_BUFFERTOOSMALL /*0x3003*/ )
if ( GetBufferPtr() != NULL )
ret = IDOK;
}
return ret;
}

template<class TBaseDialog>
void TFEFileDialog<TBaseDialog>::ParseFileNamesBuffer() const
{
if ((GetBufferPtr() != NULL) && !m_bParsed)
{
TCHAR *ptrNames = (LPTSTR)GetFileNamesPtr();
CString temp = ptrNames;

// Find out if there is more files listed
if (temp.Replace(_T("\" \""), _T("\"")) > 0)
{ // The case when there is more files:
temp.Delete(0, 1); // remove leading quote mark
temp.Delete(temp.GetLength() - 1, 1); // remove trailing space
_tcscpy((LPTSTR)ptrNames, temp);

// Replace '\"' by '\0';
// after doing this, the whole buffer will be terminated by double-zero
register TCHAR *ptrChar = ptrNames;
while (*ptrChar)
{
if ('\"' == *ptrChar)
*ptrChar = '\0';
++ptrChar;
}
}
else
{ // It is just single file name.
// Make sure that the whole buffer will be terminated by double-zero
register TCHAR *ptrEnd = ptrNames + (temp.GetLength() + 1);
*ptrEnd = '\0';
}

m_bParsed = TRUE;
}
}

template<class TBaseDialog>
POSITION TFEFileDialog<TBaseDialog>::GetStartPosition() const
{
if (GetBufferPtr() == NULL)
{
return TBaseDialog::GetStartPosition();
}
else
{
ParseFileNamesBuffer();
return (POSITION)GetFileNamesPtr();
}
}

template<class TBaseDialog>
CString TFEFileDialog<TBaseDialog>::GetNextPathName(POSITION &pos) const
{
CString strRet;

if (GetBufferPtr() == NULL)
{
strRet = TBaseDialog::GetNextPathName(pos);
}
else
{
ASSERT(pos);
TCHAR *ptr = (TCHAR *)pos;

strRet = GetBufferPtr();
strRet += _T("\\");
strRet += ptr;

ptr += _tcslen(ptr) + 1;
if (*ptr)
pos = (POSITION)ptr;
else
pos = NULL;
}

return strRet;
}

template<class TBaseDialog>
void TFEFileDialog<TBaseDialog>::OnFileNameChange()
{
TCHAR dummy_buffer;

// Get the required size (in TCHARS) for the 'files' buffer
UINT nfiles = CommDlg_OpenSave_GetSpec(GetParent()->m_hWnd, &dummy_buffer, 1);

// Get the required size (in TCHARS) for the 'folder' buffer
UINT nfolder = CommDlg_OpenSave_GetFolderPath(GetParent()->m_hWnd, &dummy_buffer, 1);

// Check if lpstrFile and nMaxFile are large enough
if (nfiles + nfolder > m_ofn.nMaxFile)
{
HWND hParent = (HWND)*GetParent();
UINT nTotalChars = nfiles + nfolder + 2;

m_buffer.SetSize(sizeof(TCHAR)*nTotalChars, N_BUFFER_GROWBY);
m_nFileNamesOffset = sizeof(TCHAR)*nfolder;
CommDlg_OpenSave_GetFolderPath(hParent, GetBufferPtr(), nfolder);
CommDlg_OpenSave_GetSpec(hParent, GetFileNamesPtr(), nfiles);
m_bParsed = FALSE;
}
else if (GetBufferPtr())
{
FreeBuffer();
}

TBaseDialog::OnFileNameChange();
}


//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif


// FECFileDialog.cpp : CFECFileDialog implementation file
//

#include "stdafx.h"
#include "FEFileDialog.h"

#ifndef _INC_CDERR
#include <cderr.h> // for FNERR_BUFFERTOSMALL
#endif // _INC_CDERR

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CFECFileDialog

IMPLEMENT_DYNAMIC(CFECFileDialog, CFileDialog)

BEGIN_MESSAGE_MAP(CFECFileDialog, CFileDialog)
//{{AFX_MSG_MAP(CFECFileDialog)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()


CFECFileDialog::CFECFileDialog(
BOOL bOpenFileDialog, LPCTSTR lpszDefExt, LPCTSTR lpszFileName,
DWORD dwFlags, LPCTSTR lpszFilter, CWnd* pParentWnd) :
TFEFileDialog<CFileDialog>(bOpenFileDialog, lpszDefExt, lpszFileName,
dwFlags, lpszFilter, pParentWnd)
{
}

CFECFileDialog::~CFECFileDialog()
{
}

/////////////////////////////////////////////////////////////////////////////
// CFECFileDialogEx

#ifdef COMPILE_DLGEXT

IMPLEMENT_DYNAMIC(CFECFileDialogEx, CFileDialogEx)

BEGIN_MESSAGE_MAP(CFECFileDialogEx, CFileDialogEx)
//{{AFX_MSG_MAP(CFECFileDialogEx)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()


CFECFileDialogEx::CFECFileDialogEx(
BOOL bOpenFileDialog, LPCTSTR lpszDefExt, LPCTSTR lpszFileName,
DWORD dwFlags, LPCTSTR lpszFilter, CWnd* pParentWnd) :
TFEFileDialog<CFileDialogEx>(bOpenFileDialog, lpszDefExt, lpszFileName,
dwFlags, lpszFilter, pParentWnd)
{
}

CFECFileDialogEx::~CFECFileDialogEx()
{
}

#endif // COMPILE_DLGEXT


QuestionPossible Bug? Pin
Defenestration16-Oct-03 10:34
Defenestration16-Oct-03 10:34 
AnswerRe: Possible Bug? Pin
Defenestration16-Oct-03 10:55
Defenestration16-Oct-03 10:55 
AnswerRe: Possible Bug? Pin
PJ Arends16-Oct-03 13:31
professionalPJ Arends16-Oct-03 13:31 
GeneralImplementation in Doc/View.. Pin
Divya Rathore29-Jul-03 22:03
Divya Rathore29-Jul-03 22:03 
GeneralFile Selection Ordering Pin
UKNCDC29-Jul-03 1:23
UKNCDC29-Jul-03 1:23 
QuestionProblem on XP? Pin
JASchmitz16-May-03 7:45
JASchmitz16-May-03 7:45 
GeneralOne Question... Pin
kadfafafadf ad6-Mar-03 8:16
kadfafafadf ad6-Mar-03 8:16 
GeneralRe: One Question... Pin
PJ Arends6-Mar-03 8:24
professionalPJ Arends6-Mar-03 8:24 
GeneralFolder selection Pin
Tine25-Feb-03 22:40
Tine25-Feb-03 22:40 
GeneralRe: Folder selection Pin
PJ Arends26-Feb-03 5:50
professionalPJ Arends26-Feb-03 5:50 
Generalgot some problem... Pin
Manikandan6-Jan-03 14:58
Manikandan6-Jan-03 14:58 
GeneralRe: got some problem... Pin
PJ Arends7-Jan-03 15:06
professionalPJ Arends7-Jan-03 15:06 
GeneralExcellent Pin
Philippe Lhoste3-Dec-02 6:26
Philippe Lhoste3-Dec-02 6:26 
GeneralGood job Pin
Shog924-Nov-02 15:00
sitebuilderShog924-Nov-02 15:00 
GeneralRe: Good job Pin
PJ Arends25-Nov-02 5:50
professionalPJ Arends25-Nov-02 5:50 
GeneralRe: Good job Pin
Shog927-Feb-03 11:57
sitebuilderShog927-Feb-03 11:57 
GeneralRe: Good job Pin
PJ Arends27-Feb-03 16:35
professionalPJ Arends27-Feb-03 16:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.