Environment: VC6, W9x, W2K
Introduction
When I need a user to select a file or folder, I would have to
create two controls, an edit control for entering the text, and a
browse button that would bring up a dialog for actually choosing
the file or folder. So I thought why no combine the two controls
into one. The CFileEditCtrl class is the result. The
class definition and implementation are in the files FileEditCtrl.h
and FileEditCtrl.cpp which are included in the demo
project.
Testing
This code has not been tested for UNICODE builds, nor has it
been tested on a network with UNC paths. If any bugs are found
and fixed, please drop me a note at [email protected]
Acknowledgements
Thanks to Michael Dunn for his article "Introduction
to COM – What It Is and How to Use It" for showing me
how to handle shortcut (*.lnk) files.
Features
- The control is derived from CEdit. All CEdit member
functions are CFileEditCtrl member functions. It can
be created with any of the ES_* edit control styles.
It will respond just like any other edit control to any EM_*
commands, and sends all EN_* notification messages. - The ellipses button is drawn in the controls
nonclient area. It is a part of the control, not a seperate
button that has to be added onto the dialog template, or
otherwise created or setup. It can be placed on either the left
or the right side of the control. - The control has its own DDX_FileEditCtrl
and DDV_FileEditCtrl dialog data exchange functions.
Setting up the control for use is very easy. - Using the Create() member function,
the control can be created in any window, not just dialogs or
forms. - The control can be used to browse for files or
folders. And it has a member function that can be used to switch
between the two. When the CFileDialog or SHBrowseForFolder
dialogs are opened, they will be set to the directory currently
entered in the control. - The control accepts relative paths. Users can enter
..\..\anyfolder and the control will return the
absolute path relative to the current working directory. Entering
‘.’ will return the current working directory. If
the FEC_MULTIPLE flag is set, the first file entered
will be relative to the current directory, and all subsequent
files will be relative to the first file, unless the absolute
path is entered. - The control accepts wildcards ( ‘*’ and/or
‘?’ ) in the file name. Just set the FEC_WILDCARDS
flag. - The control will automatically
dereference shortcut ( *.lnk ) files. To disable this feature,
just set the FEC_NODEREFERENCELINKS flag - The control accepts Drag and Drop files and folders.
Just create it with the WS_EX_ACCEPTFILES extended
widows style. - Member functions give access to the internal BROWSEINFO
and OPENFILENAME structures, so if the default
settings are not satisfactory, there is complete control over how
the CFileDialog and SHBrowseForFolder
dialogs are implemented. - The control is resizable. The button keeps its
proportionate size relative to the height of the control. As the
control gets taller, the button gets bigger, and so do the dots
on the button. - The CFileDialog has the text on its
default button changed from ‘Open’ to ‘OK’. - When the ellipses button is clicked, the control
sends a WM_NOTIFY message to its parent window,
giving the parent window a chance to stop the SHBrowseForFolder
or CFileDialog from popping up. The <Ctrl><.>
keystroke has the same action as a click on the button.
Using the Control
To use this control in your application, Add the FileEditCtrl.h
and FileEditCtrl.cpp files to your project. Then it is
recommended to add the text strings defined at the top of the FileEditCtrl.cpp
file to your string table resource, using the FEC_IDS_*
identifiers defined there.
// FEC_IDS_ALLFILES will be defined in resource.h if these strings
// are in a string table resource
#if !defined FEC_IDS_ALLFILES
#define FEC_NORESOURCESTRINGS so this class knows how to handle these strings
#define FEC_IDS_ALLFILES _T("All Files (*.*)|*.*||")
#define FEC_IDS_BUTTONTIP _T("Browse")
#define FEC_IDS_FILEDIALOGTITLE _T("Browse for File"
#define FEC_IDS_SEPERATOR _T(";")
#define FEC_IDS_NOFILE _T("Enter an existing file.")
#define FEC_IDS_NOTEXIST _T("%s does not exist.")
#define FEC_IDS_NOTFILE _T("%s is not a file.")
#define FEC_IDS_NOTFOLDER _T("%s is not a folder.")
#define FEC_IDS_OKBUTTON _T("OK")
#endif
To use the control on a dialog, using all the default
settings, add an edit control to the dialog template, add a
CString member variable to the dialog class, and in DoDataExchange()
add the DDX_FileEditCtrl() and DDV_FileEditCtrl()
functions. The default settings for the SHBrowseForFolder
dialog has the BIF_RETURNONLYFSDIRS flag set. And
the default settings for the CFileDialog dialog has
the OFN_HIDEREADONLY, OFN_FILEMUSTEXIST
and OFN_NOCHANGEDIR flags set, the file filter is
set to the FEC_IDS_ALLFILES resource string, and the
dialog caption is set to the FEC_IDS_FILEDIALOGTITLE
resource string. If you want the control to be used for folders,
set the flag in last parameter in DDX_FileEditCtrl()
to FEC_FOLDER, set it to FEC_FILE for
files.
If you want more control over the dialogs, such as choosing
multiple files, then you have to add a CFileEditCtrl
variable to your dialog class. In DoDataExchange()
add the second version of DDX_FileEditCtrl(). And
then get a pointer to the OPENFILENAME or BROWSEINFO
structures, using the GetOpenFileName() or GetBrowseInfo()
functions, and set them accordingly. In the demo app I have an
edit control with an ID of IDC_EDIT1 and a CFileEditCtrl
variable m_FileEditCtrl.
void CFileEditDemoDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(CFileEditDemoDlg) ... //}}AFX_DATA_MAP DDX_FileEditCtrl(pDX, IDC_EDIT1, m_FileEditCtrl, FEC_FILE); ... }
Because these functions are not supported by Class Wizard,
they have to be placed outside the AFX_DATA_MAP code
block. If you would like to add Class Wizard support, see MFC
Technical Note 26 DDX and DDV routines and look under ClassWizard
To retrieve the file names from the control, use the GetStartPosition()
and GetNextPathName() member functions. In the demo
app I did this in the CDumpDialog::OnInitDialog()
function in order to fill the list box with the files entered by
the user.
BOOL CDumpDialog::OnInitDialog() { CDialog::OnInitDialog(); CFileEditDemoDlg *pDemo = (CFileEditDemoDlg *)GetParent(); int width = 0; CString str; CDC *pDC = m_List.GetDC(); int saved = pDC->SaveDC(); pDC->SelectObject(GetFont()); // call GetStartPosition() to get the position of the first // file in the control POSITION pos = pDemo->m_fileeditctrl.GetStartPosition(); while (pos) { // add the file paths to the list str = pDemo->m_fileeditctrl.GetNextPathName(pos); m_List.AddString(str); CSize size(0, 0); size = pDC->GetTextExtent(str); width = width > size.cx ? width : size.cx; } pDC->RestoreDC(saved); ReleaseDC(pDC); m_List.SetHorizontalExtent(width + 5); return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE }
When a user clicks on the browse button, the control will send
a WM_NOTIFY message with a FEC_NM_PREBROWSE
notification code to it’s parent window before it brings up the SHBrowseForFolder
or CFileDialog dialogs. The NMHDR*
pointer will point to a FEC_NOTIFY structure. The pFEC
member will point to the CFileEditCtrl that sent the
message. You can use this pointer to modify the OPENFILENAME
or BROWSEINFO structures. If you set the LRESULT
parameter of the OnNotify handler to a nonzero value, you will
stop the dialogs from executing.
The control will send another WM_NOTIFY message
with a FEC_NM_POSTBROWSE notification code after the
dialogs return and the controls window text has been updated. The
NMHDR* pointer will once again point to a FEC_NOTIFY
structure, but the LRESULT parameter will have no effect and can
be ignored.
typedef struct tagFEC_NOTIFY { NMHDR hdr; CFileEditCtrl* pFEC; // pointer to control that sends // this notification tagFEC_NOTIFY (CFileEditCtrl *FEC, UINT code); } FEC_NOTIFY; #define FEC_NM_PREBROWSE 1 // notification code sent before dialogs // pop up #define FEC_NM_POSTBROWSE 2 // notification code sent after dialogs // return
User Functions
These are the functions that are used to control the CFileEditCtrl
class
CFileEditCtrl::CFileEditCtrl(BOOL bAutoDelete /* = FALSE */)
The class constructor has a parameter bAutoDelete
that is FALSE by default. Setting bAutoDelete
to TRUE causes the control class to delete itself in
its PostNCDestroy() function. If this is done, there
is no way of getting the files entered after the dialog has
closed.
CFileEditCtrl::~CFileEditCtrl()
The class destructor cleans up all the internal pointers.
BOOL CFileEditCtrl::Create(DWORD dwFlags, DWORD dwExStyle, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID)
Create creates a CFileEditCtrl
window in any window that does not have a template. For
information on the dwFlags parameter see SetFlags()
below. All other parameters are passed on to CWnd::CreateEx().
Returns TRUE on success, and FALSE on
failure.
DWORD CFileEditCtrl::GetFlags()
GetFlags() returns a DWORD containing the bit
flags. See SetFlags() below for an explanation of
the flags.
BOOL CFileEditCtrl::ModifyFlags(DWORD remove, DWORD add)
ModifyFlags() is used to modify the controls
functionality. First the remove flags are removed,
and then the add flags are added. Returns TRUE
on success, and FALSE on failure. See SetFlags()
below for an explanation of the flags.
BOOL CFileEditCtrl::SetFlags(DWORD dwFlags)
SetFlags() is used to set the controls
functionality. SetFlags() returns TRUE
on success, and FALSE on failure.
dwFlag |
Purpose |
FEC_FILE | The control is set to accept files. When the ellipses button is clicked, the control starts the windows common File Open dialog. This flag cannot be used with the FEC_FOLDER flag. |
FEC_MULTIPLE | Used with FEC_FILE. The control will accept multiple files. Has the same effect as the OFN_ALLOWMULTISELECT flag. |
FEC_MULTIPLEFILES | Combination of FEC_FILE and FEC_MULTIPLE |
FEC_WILDCARDS | Used with FEC_FILE. The control will accept and resolve any wildcards (‘*’ and/or ‘?’) in the file name. If the FEC_MULTIPLE flag is set, GetNextPathName() will return all the files that match. If FEC_MULTIPLE is not set, GetNextPathName() will return only the first match. |
FEC_NODEREFERENCELINKS | Used with FEC_FILE. GetNextPathName() will return the path name of any shortcut (*.lnk) files entered. If this flag is not set, GetNextPathName() will return the path name of the file the shortcut points to. Has the same effect as the OFN_NODEREFERENCELINKS flag. |
FEC_FOLDER | The control is set to accept folders. When the ellipses button is clicked, the control starts the SHBrowseForfolder dialog. This flag cannot be used with the FEC_FILE flag. |
FEC_TRAILINGSLASH | Used with FEC_FOLDER. The folder path entered in the control will have a trailing slash. |
FEC_BUTTONLEFT | The ellipses button will be placed on the left side of the control. |
FEC_BUTTONTIP | Enables the browse button tooltip. The tooltip text is set with the FEC_IDS_BUTTONTIP resource string |
FEC_CLIENTTIP | Enables the client area tooltip. The tooltip text is set with the SetClientTipText() member function |
SetClientTipText(CString text)
SetClientTipText() is used to set the text of the
client area tooltip.
BROWSEINFO* CFileEditCtrl::GetBrowseInfo() const
GetBrowseInfo() returns a pointer to the internal
BROWSEINFO structure. The return value is NULL
if the control is set to find files. Use this pointer to modify
the SHBrowseForFolder dialog.
OPENFILENAME* CFileEditCtrl::GetOpenFileName() const
GetOpenFileName() returns a pointer to the
internal OPENFILENAME structure. The return value is
NULL if the control is set to find folders. Use this
pointer to modify the CFileDialog dialog.
POSITION CFileEditCtrl::GetStartPosition()
GetStartPosition() returns a MFC POSITION
structure that is used as a starting point for the GetNextPathName()
function. Returns NULL if there are no files entered
in the control.
CString CFileEditCtrl::GetNextPathName(POSITION &pos)
GetNextPathName() returns a CString containing
the full path name of the file entered in the control at the
position referenced by the pos variable. GetNextPathName()
updates pos to reference the next file entered in the control, or
sets pos to NULL if there are no more
files. Before calling GetNextPathName() for the
first time, pos must be initialized by the GetStartPosition()
function.
void DDV_FileEditCtrl (CDataExchange *pDX, int nIDC)
DDV_FileEditCtrl() is used to check that the
entered file actually exists, and if it does, ensures that the
user has entered either a file or a folder, depending on on the
settings of the control. If you want your user to enter a
nonexistant file or folder, do not use the DDV_FileEditCtrl()
function.
void DDX_FileEditCtrl (CDataExchange *pDX, int nIDC, CFileEditCtrl &rCFEC, DWORD dwFlags)
void DDX_FileEditCtrl (CDataExchange *pDX, int nIDC, CString& rStr, DWORD dwFlags)
These functions subclass the edit controls with the ID nIDC
and pass the file data between the control and either the CFileEditCtrl
referenced by rCFEC or the CString
referenced by rStr. For information on dwFlags
see SetFlags() above. The CString
version does not accept the FEC_MULTIPLE flag (How can it
return multiple files in one CString?).
The Button
In order to get the button to work I first had to override the
OnNcCalcSize() function. This is the function that
is used to calculate the size and position of a windows client
area. In my override I called CEdit::OnNcCalcSize()
to get the default size and position of the client area, then I
adjusted the size of the client area and calculated the size and
position of the button. The CRect m_rcButtonRect
member variable is used to store this information.
void CFileEditCtrl::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS FAR* lpncsp) { // calculate the size of the client area and the button CEdit::OnNcCalcSize(bCalcValidRects, lpncsp); // set button area equal to client area of edit control m_rcButtonRect = lpncsp->rgrc[0]; if (m_bButtonLeft) // draw button on left side of the control { // shrink left side of client area by 80% of the // height of client area lpncsp->rgrc[0].left += (lpncsp->rgrc[0].bottom - lpncsp->rgrc[0].top) * 8/10; // shrink button so its right side is at left side of client area m_rcButtonRect.right = lpncsp->rgrc[0].left; } else // draw the button on the right side of the control { // shrink right side of client area by 80% of height of client area lpncsp->rgrc[0].right -= (lpncsp->rgrc[0].bottom - lpncsp->rgrc[0].top) * 8/10; // shrink button so its left side is at right side of client area m_rcButtonRect.left = lpncsp->rgrc[0].right; } if (bCalcValidRects) // convert button coordinates from parent client coordinates // to control window coordinates m_rcButtonRect.OffsetRect(-lpncsp->rgrc[1].left, -lpncsp->rgrc[1].top); m_rcButtonRect.NormalizeRect(); }
The only time OnNcCalcSize() is called is when
the windows frame has changed, so to force a call to OnNcCalcSize()
I had to call SetWindowPos() from SetFlags(),
using the SWP_FRAMECHANGED flag.
BOOL FileEditCtrl::SetFlags(DWORD dwFlags)
{
...
// Force a call to CFileEditCtrl::OnNcCalcSize() to calculate button size
SetWindowPos(NULL,0,0,0,0,SWP_FRAMECHANGED|SWP_NOMOVE|SWP_NOSIZE|
SWP_NOZORDER|SWP_NOACTIVATE);
...
}
I then needed to paint the button on the control, so I wrote
the DrawButton() function. Because the button is not
in the client area of the window, DrawButton() has
to be called from OnNcPaint().
void CFileEditCtrl::OnNcPaint() { CEdit::OnNcPaint(); // draws the border around the control DrawButton (m_nButtonState); // draw the button in its current state }
The next thing was to get mouse messages for the button.
Because the button is not in the client area, it would not get
client area mouse messages, and because it is not a border, it
would not get nonclient mouse messages. To solve this problem I
had to override OnNcHitTest() and get it to return HT_BORDER
when the mouse cursor was over the button.
UINT CFileEditCtrl::OnNcHitTest(CPoint point)
{
...
UINT where = CEdit::OnNcHitTest(point);
if (where == HTNOWHERE && ScreenPointInButtonRect(point))
where = HTBORDER;
return where;
}
Now a mouse press on the button would generate a WM_NCLBUTTONDOWN
message, so I had to override OnNcLButtonDown(). In OnNcLButtonDown()
I would capture the mouse using SetCapture() and
call DrawButton() to draw the button as down.
Because once the mouse is captured, it no longer generates
nonclient mouse messages, I would have to respond to WM_LBUTTONUP
and WM_MOUSEMOVE messages in order to keep track of
the mouse. Because CEdit::OnLButtonDown also
captures the mouse, I could not use GetCapture to
see if the button had captured the mouse, so I added the BOOL
m_bMouseCaptured variable to keep track of it.
void CFileEditCtrl::OnNcLButtonDown(UINT nHitTest, CPoint point) { CEdit::OnNcLButtonDown(nHitTest, point); ... if (ScreenPointInButtonRect(point)) { SetCapture(); m_bMouseCaptured = TRUE; SetFocus(); DrawButton(BTN_DOWN); } }
By overriding OnMouseMove() I could keep track of
the captured mouse, and draw the button as down if the mouse
cursor was over the button, or as up if it was not.
void CFileEditCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
CEdit::OnMouseMove(nFlags, point);
...
if (m_bMouseCaptured)
{
ClientToScreen(point);
if (ScreenPointInButtonRect(point))
{
if (m_nButtonState != BTN_DOWN)
DrawButton (BTN_DOWN);
}
else if (m_nButtonState != BTN_UP)
DrawButton (BTN_UP);
}
}
In the override of OnLButtonUp the mouse capture
is released, the m_bMouseCaptured flag is cleared,
and if the mouse cursor is over the button, the ButtonClicked()
function is called. The ButtonClicked() function
opens the appropriate dialog and posts a BN_CLICKED
notification message to the control’s parent window.
void CFileEditCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
CEdit::OnLButtonUp(nFlags, point);
...
if (m_bMouseCaptured)
{
ReleaseCapture();
m_bMouseCaptured = FALSE;
if (m_nButtonState != BTN_UP)
DrawButton(BTN_UP);
ClientToScreen(point);
if (ScreenPointInButtonRect(point))
ButtonClicked();
}
}
Revision History
November 11, 2000 - allowed the control to work with dialog templates November 22, 2000 - register the control's window class, can now be added to dialog as custom control January 4, 2001 - near total rewrite of the control, now derived from CEdit - control can now be added to dialog template using an edit control - browse button now drawn in nonclient area of control January 5, 2001 - removed OnKillFocus(), replaced with OnDestroy() January 15, 2001 - added DDX_ and DDV_ support - modified GetStartPosition() and GetNextPathName() - modified how FECOpenFile() updates the control text when multiple files are selected - added FillBuffers() - added support for relative paths - added OnChange handler - added drag and drop support January 26, 2001 - fixed bug where SHBrowseForFolder does not like trailing slash January 27, 2001 - fixed bug where if control is initialized with text, FillBuffers was not called. January 28, 2001 - removed GetFindFolder() and SetFindFolder() replaced with GetFlags() and SetFlags() - modified the DDX_ and DDV_ functions to accept these flags - modified the Create() function to accept these flags - allowed option for returned folder to contain trailing slash - allowed browse button to be on the left side of the control - added ScreenPointInButtonRect() to better tell if mouse cursor is over the button - modified how OnDropFiles() updates the control text when multiple files are dropped February 25, 2001 - fixed EN_CHANGE notification bug. Now parent window recieves this notification message used ON_CONTROL_REFLECT_EX macro instead of ON_CONTROL_REFLECT April 12, 2001 - added OnSize handler, fixed button drawing problem when control size changed April 21, 2001 - added a tooltip for the browse button May 12, 2001 - removed OnDestroy, replaced with PostNCDestroy - added tooltip support to client area - modified the FECBrowseForFolder and FECFolderProc functions - added a one pixel neutral area between the client area and browse button when the button is on the right hand side of the control. (looks better IMO) May 29, 2001 - PL -- removed the filename from the m_pCFileDialog->m_ofn.lpstrInitialDir variable, so when browsing back for file, we open the correct folder. - used smaller (exact size) arrays for file, extension and path components. - some cosmetic changes. May 29, 2001 - FECFolderProc now checks for UNC path. SHBrowseForFolder can not be initialized with UNC June 2, 2001 - modified ButtonClicked function. Now sends a WM_NOTIFY message to parent window before showing dialog, allows parent window to cancel action by setting result to nonzero. also sends WM_NOTIFY message to parent window after dialog closes with successful return June 9, 2001 - added OnNcLButtonDblClk handler. Double click on button treated as two single clicks June 23, 2001 - placed a declaration for the FECFolderProc global callback function into the header file - fixed bug that occured when removing the filename from the m_pCFileDialog->m_ofn.lpstrInitialDir variable when there was no file to remove August 2, 2001 - replaced SetWindowText() with OnSetText() message handler. now correctly handles WM_SETTEXT messages August 12, 2001 - added GetValidFolder() function and modified FECOpenFile() function. we now start browsing in the correct folder -- it finally works!!! {:o) - modified SetFlags() so the button could be moved by setting the FEC_BUTTONLEFT flag - removed the m_bCreatingControl variable - removed the call to SetWindowPos() from the Create() and DDX_FileEditCtrl() functions. Now done in SetFlags() function August 14, 2001 - modified FECOpenFile(). Now sets the file name in CFileDialog to first file name in FileEditCtrl August 18, 2001 - Set the tooltip font to the same font used in the CFileEditCtrl September 2, 2001 - added the ModifyFlags() function and changed how the flags are handled - modified the GetFlags() function - added the FEC_MULTIPLE and FEC_MULTIPLEFILES flags - added support for wildcards ( '*' and '?') in filenames Involved : modifying the GetStartPosition(), GetNextPathName(), SetFlags(), and FillBuffers() functions adding the ExpandWildCards() function replacing the m_lpstrFiles variable with the m_Files array adding the FEC_WILDCARDS flag. September 3, 2001 - added ability to dereference shortcut files (*.lnk) - added the FEC_NODEREFERENCELINKS flag. - added the DereferenceLink() function. September 5, 2001 - fixed the Create() function - now destroys the control if the SetFlags() function fails September 8, 2001 - added the AddFiles() function to be better able to handle shortcut (*.lnk) files modified the OnDropFiles() function to be better able to handle shortcut (*.lnk) files
Be sure to check here for the latest updates.