Introduction
In this article, I show you how to recreate some of the browsing functionality that is seen in Windows Explorer.
There is a tree control on the left with which the user could navigate folders - causing the files in whichever folder was currently selected to appear in the right hand pane.
Of course, if you want to have a program that enhances Windows Explorer, then you should probably look at working with the Shell. I haven't done that because I want to explore the way the user interface works and potentially build similar interfaces for other applications which navigate other sorts of information than folders and files in storage devices.
Here is a description of how to build my project from scratch...
Select 'File' then 'New'. Give your project the name FTreeBrowser
.
Click 'OK'. Choose whether you want a single document or a multiple document interface. Check the 'Document/View architecture support?' (When I did this, it was already checked).
Click through steps 2, 3 and 4, no action is required here. In step 5, select the 'Windows Explorer' option under the question 'What style of project would you like?'
Click 'Next'.
Next you get the option to change some class and file names. For CFTreeBrowserView
, use the following names instead of the automatically generated ones:
CFTreeBrowserView
Class Name: CRightView
Header File: RightView.h
Implementation File: RightView.cpp
Base Class: CListView
Click 'OK' in the 'New Project Information' pane.
In the Workspace pane, click on the FileView tab. Open up the Header Files folder and open the file FTreeBrowserDoc.h. In this file, insert the lines...
class CLeftView;
class CRightView;
... and...
CLeftView *pLeftView;
CRightView *pRightView;
... in the positions shown in the code section below:
....
#if _MSC_VER > 1000
#pragma once
#endif
class CLeftView;
class CRightView;
class CFTreeBrowserDoc : public CDocument
{
protected:
CFTreeBrowserDoc();
DECLARE_DYNCREATE(CFTreeBrowserDoc)
public:
public:
CLeftView *pLeftView;
CRightView *pRightView;
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
public:
virtual ~CFTreeBrowserDoc();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
protected:
functions here.
generated code !
DECLARE_MESSAGE_MAP()
};
Open up the ClassWizard (press CTRL-W), under the Message Maps tab, select CLeftView
in the 'Class name' list, CLeftView
in the 'Object IDs' list, WM_CREATE
in the 'Messages:' list. If you double-click on WM_CREATE
, the OnCreate
function is created and highlighted for you in the member functions list. Click on 'OK'.
Go back to the workspace, click the ClassView tab and open up the CLeftView
class. If you double-click on the OnCreate
function, the new function will be presented to you in the file editor pane.
Add the line GetDocument()->pLeftView = this;
so that the function appears as below:
int CLeftView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CTreeView::OnCreate(lpCreateStruct) == -1)
return -1;
GetDocument()->pLeftView = this;
return 0;
}
Repeat the last few steps from 'Open up the ClassWizard' but this time select CRightView
in the 'Class name' list and CRightView
in the 'Object IDs' list. Next you will need to add the line GetDocument()->pRightView = this;
to the CRightView::OnCreate
function as follows:
int CRightView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CListView::OnCreate(lpCreateStruct) == -1)
return -1;
GetDocument()->pRightView = this;
return 0;
}
Next, add five icons. You create these side by side in a bitmap 16 pixels high and 80 pixels wide. The first two will be used for the root nodes (unselected and selected respectively), the third and fourth icons will be used for branch nodes (unselected and selected respectively) and the last icon will be used for leaf nodes. You can do this by clicking 'Insert' on the main menu and then selecting resource. Double click on 'Bitmap'.
Right hand click on the icon for the bitmap that has appeared in the resource tab of the ClassView pane. Change the ID of the bitmap to IDB_TREE_BMP
. By using the grab buttons at the edge of the pixel grid, set its width to 90 and its height to 16. You don't have to count the pixels, the size of the bitmap appears in the bottom right hand corner of the IDE main frame. Draw your icons in boxes of 16 X 16 pixels across the bitmap.
Once you have made your icons, you make them accessible to your tree control. The first step in this process is declaring a CImageList
variable. Open up ClassView. Right click over CLeftView
and click on 'Add Member Variable'. Declare your variable type as CImageList
, variable Name as m_TreeImages
and access as Public
.
Now to populate the root items in the tree we create a function called CreateRoots
. In ClassView, right-click CLeftView
-> Add Member Function. Set the return type to void
. Set the Function Declaration to CreateRoots
. Leave the access as Public
.
Put the following code in the new function:
void CLeftView::CreateRoots()
{
GetTreeCtrl().DeleteAllItems();
CTreeCtrl &ctlDrives = this->GetTreeCtrl();
m_TreeImages.Create(IDB_TREE_BMP, 16, 1, RGB(255, 255, 255));
ctlDrives.SetImageList(&m_TreeImages, TVSIL_NORMAL);
HTREEITEM hRoot;
char * strBuffer= NULL;
CString strMessage;
int nPos = 0;
UINT nCount = 0;
CString strDrive = "?:\\";
DWORD dwDriveList = ::GetLogicalDrives ();
CString cTmp;
while (dwDriveList) {
if (dwDriveList & 1) {
cTmp = strDrive;
strDrive.SetAt (0, 0x41 + nPos);
strDrive = strDrive.Left(2);
hRoot = ctlDrives.InsertItem(strDrive,0, 1);
}
dwDriveList >>= 1;
nPos++;
}
}
We want this function to be called when the application is started up or whenever a new document is made. In ClassView, open up the CFTreeBrowserDoc
node and double-click on the OnNewDocument
node.
Place a call to the CreateRoots
function here. The OnNewDocument
function should now look like this:
BOOL CFTreeBrowserDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
this->pLeftView->CreateRoots();
return TRUE;
}
Next, scroll up to the top of the file and add an #include
line for LeftView.h.
#include "stdafx.h"
#include "FTreeBrowser.h"
#include "FTreeBrowserDoc.h"
#include "LeftView.h"
You should now be able to compile your project. The resulting application should show your system drives displayed in the left hand pane - but nothing else yet.
Go back to the CLeftView
class and add a call to the tree's ModifyStyle
method in the OnInitialUpdate
function.
void CLeftView::OnInitialUpdate()
{
CTreeView::OnInitialUpdate();
GetTreeCtrl().ModifyStyle(NULL, TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT);
}
To set the width of the left pane, open up the CMainFrame
class (or CChildFrame
class if you are writing an MDI application) and edit the OnCreateClient
function. If you had created an SDI application, rather than an MDI application, you would find this function in the CMainFrame
class.
To set a width of 250, place '250' in the conditional line below, as follows:
if (!m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CLeftView),
CSize(250, 100), pContext) ||
!m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CRightView),
CSize(100, 100), pContext))
Now we will pay some attention to the right hand pane. First we'll add a function to clear the list control. In ClassView, right-click CRightView
and select AddMemberFunction
. Set the return type to void
and the function name to ResetFiles
. Keep the function's access as public
and click enter. You can now add the following code:
void CRightView::ResetFiles()
{
CListCtrl &ctlLView = GetListCtrl();
ctlLView.DeleteAllItems();
while(ctlLView.DeleteColumn(0))
;
UpdateWindow();
}
It is now time to add the functionality which will display the contents of whatever folder is selected in the left pane. The contents are displayed in the right pane and the new function is added to CRightView
. The type of this function is void
, the implementation is DisplayFiles(LPTSTR Path)
and the access is public
.
Fill the function with the following code:
void CRightView::DisplayFiles(LPTSTR Path)
{
CListCtrl &ctlRightView = this->GetListCtrl();
ResetFiles();
ctlRightView.InsertColumn(0, _T("File Name"), LVCFMT_LEFT, 160);
ctlRightView.InsertColumn(1, _T("File Size"), LVCFMT_LEFT, 40);
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
int nItem;
hFind = FindFirstFile(Path, &FindFileData);
int n = 0;
if (hFind != INVALID_HANDLE_VALUE){
do {
if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
&& FindFileData.cFileName != CString (".")
&& FindFileData.cFileName != CString (".."))
{
++n;
nItem = ctlRightView.InsertItem(n, "File",2);
ctlRightView.SetItemText(nItem, 0, FindFileData.cFileName);
long lFSize = FindFileData.nFileSizeLow;
CString strFSize="";
strFSize.Format("%d",lFSize);
ctlRightView.SetItemText(nItem, 1, strFSize.GetBuffer(1));
}
}while((::WaitForSingleObject(m_hStopEvent, 0) !=
WAIT_OBJECT_0) && (::FindNextFile(hFind, &FindFileData)));
::FindClose(hFind);;
hFind = FindFirstFile(Path, &FindFileData);
do {
if (!(FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)){
++n;
nItem = ctlRightView.InsertItem(n, "File",4);
ctlRightView.SetItemText(nItem, 0, FindFileData.cFileName);
long lFSize = FindFileData.nFileSizeLow;
CString strFSize="";
strFSize.Format("%d",lFSize);
ctlRightView.SetItemText(nItem, 1, strFSize.GetBuffer(1));
}
}while((::WaitForSingleObject(m_hStopEvent, 0) !=
WAIT_OBJECT_0) && (::FindNextFile(hFind, &FindFileData)));
::FindClose(hFind);;
}
}
Before you can compile and run the application at this stage, you need to add the m_hStopEvent
handle. To do this, you need to open up the file RightView.h and add the line HANDLE m_hStopEvent;
to the class definition in the private:
list as can be seen in this listing of the file:
...
#if _MSC_VER > 1000
#pragma once
#endif
class CRightView : public CListView
{
protected:
CRightView();
DECLARE_DYNCREATE(CRightView);
public:
CExerciseDoc* GetDocument();
public:
public:
virtual void OnDraw(CDC* pDC);
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:
virtual void OnInitialUpdate();
virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
public:
CImageList m_ListImages;
void DisplayFiles(LPTSTR Path);
void DisplaySeason(CString League, CString Season);
void DisplayLeague(CString League);
void DisplayLeagues();
void ResetLeagues();
virtual ~CRightView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& dc) const;
#endif
protected:
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnDblclk(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnStyleChanged(int nStyleType, LPSTYLESTRUCT lpStyleStruct);
DECLARE_MESSAGE_MAP()
private:
HANDLE m_hStopEvent;
};
#ifndef _DEBUG
inline CExerciseDoc* CRightView::GetDocument()
{ return (CExerciseDoc*)m_pDocument; }
#endif
...
Next we look at what happens when a folder is selected (opened) in the left hand pane. Some of this functionality is required when a folder is opened in the right hand pane, so we will make a separate function outside of the LeftView
event handler which deals with this section of the code. This function is void
, has a declaration of OpenFolder(CString CStrPath)
and should be left as public
.
Again, the lines...
private:
HANDLE m_hStopEvent;
... must be inserted at the end of the class definition in the LeftView.h file.
Now that we have the necessary functions, we can react when a node changes in the tree view.
Using either the ClassWizard, the Messages section of the Properties, or whatever, generate a TVN_SELCHANGED
message for the CLeftView
class. Use the suggested function name of OnSelChanged
.
Fill out this function as follows:
void CLeftView::OnSelchanged(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_TREEVIEW* pNMTreeView = (NM_TREEVIEW*)pNMHDR;
*pResult = 0;
CFTreeBrowserDoc *pDoc = GetDocument();
CTreeCtrl &ctlFiles = this->GetTreeCtrl();
HTREEITEM nodSelected = ctlFiles.GetSelectedItem();
CString strSelected = ctlFiles.GetItemText(nodSelected);
HTREEITEM nodParent = nodSelected;
do {
nodParent = ctlFiles.GetParentItem(nodParent);
if (nodParent!=NULL)
strSelected = ctlFiles.GetItemText(nodParent) + "\\" + strSelected;
} while (nodParent != NULL);
CString strSearchPath = strSelected + "\\*.*";
pDoc->pRightView->DisplayFiles(strSearchPath.GetBuffer(1));
OpenFolder(strSelected);
}
You will need to make sure you have an #include
line for Rightview.h at the top of the file now, as follows:
#include "stdafx.h"
#include "FTreeBrowser.h"
#include "FTreeBrowserDoc.h"
#include "LeftView.h"
#include "RightView.h"
You will notice, if you execute the program at this point, that there are no icons associated with files or folders in the right hand pane yet. The code in the function DisplayFiles
is already set to use the third and fifth icons from the icon list associated with the list control (for folders and files respectively. Currently there is no icon list associated with the control. To remedy this, open up CRightView::OnInitialUpdate()
function and add the lines as have been added below:
void CRightView::OnInitialUpdate()
{
CListView::OnInitialUpdate();
CListCtrl &ctlFiles = this->GetListCtrl();
ctlFiles.ModifyStyle(NULL, LVS_REPORT);
m_ListImages.Create(IDB_TREE_BMP, 16, 1, RGB(255, 255, 255));
ctlFiles.SetImageList(&m_ListImages, LVSIL_SMALL);
}
Before this works, you need to add a class variable to CRightView
of type CImageList
, name m_ListImages
and public
access.
To make this application behave a little bit more like Microsoft's Windows Explorer, you make a folder clicked on in the right pane become the selected folder in the left pane. To do this, create a function for CRightView
to handle the =NM_DBLCLK
message. Fill out the function as follows:
void CRightView::OnDblclk(NMHDR* pNMHDR, LRESULT* pResult)
{
*pResult = 0;
CExerciseDoc *pDoc = GetDocument();
CListCtrl &ctlRightView = this->GetListCtrl();
int nItem = ctlRightView.GetSelectionMark();
CString string= ctlRightView.GetItemText(nItem,0);
pDoc->pLeftView->SetFocus();
pDoc->pLeftView->OnRightViewFolderSelected(string, nItem);
}
Next you should add #include "LeftView.h
" at the top of the file so the #includes
section looks like this:
#include "stdafx.h"
#include "FTreeBrowser.h"
#include "LeftView.h"
#include "FTreeBrowserDoc.h"
#include "RightView.h"
Then you need to create the function CLeftView::OnRightViewFolderSelected(...)
. Make the return type void
, access public
and declaration OnRightViewFolderSelected(CString strPath, UINT index)
. You can now fill out the function with the following code:
void CLeftView::OnRightViewFolderSelected(CString strPath, UINT
index)
{
CFTreeBrowserDoc *pDoc = GetDocument();
CTreeCtrl &ctlFolders = this->GetTreeCtrl();
HTREEITEM nodSelected = ctlFolders.GetSelectedItem();
ctlFolders.Expand(nodSelected, TVE_EXPAND);
int count=0;
HTREEITEM nodChild;
nodChild = ctlFolders.GetChildItem(nodSelected);
if (index > 0){
do{
nodChild = ctlFolders.GetNextItem(nodChild,TVGN_NEXT);
++count;
}while (count < (int)index);
}
if (nodChild != NULL)
{
ctlFolders.SelectItem(nodChild);
ctlFolders.Expand(nodChild, TVE_EXPAND);
nodSelected = nodChild;
CString strSelected = ctlFolders.GetItemText(nodSelected);
HTREEITEM nodParent = nodSelected;
do {
nodParent = ctlFolders.GetParentItem(nodParent);
if (nodParent!=NULL)
strSelected = ctlFolders.GetItemText(nodParent) +
"\\" + strSelected;
} while (nodParent != NULL);
CString strSearchPath = strSelected + "\\*.*";
pDoc->pRightView->DisplayFiles(strSearchPath.GetBuffer(1));
}
}
Lastly, you need to populate the CLeftView::OpenFolder(...)
function as follows...
void CLeftView::OpenFolder(CString CStrPath)
{
CTreeCtrl &ctlFolders = this->GetTreeCtrl();
HTREEITEM hRoot;
HTREEITEM hFolder;
hRoot = ctlFolders.GetSelectedItem();
HTREEITEM hChild = ctlFolders.GetChildItem(hRoot);
while(hChild != 0){
ctlFolders.DeleteItem(hChild);
hChild = ctlFolders.GetChildItem(hRoot);
}
WIN32_FIND_DATA FindFileData;
HANDLE hFind;
CStrPath = CStrPath + "\\*.*";
hFind = FindFirstFile(CStrPath, &FindFileData);
if (hFind != INVALID_HANDLE_VALUE){
do {
long lFSize = FindFileData.nFileSizeLow;
CString strFSize="";
if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY
&& FindFileData.cFileName != CString (".")
&& FindFileData.cFileName != CString (".."))
hFolder = ctlFolders.InsertItem
(FindFileData.cFileName,2,3,hRoot);
}while((::WaitForSingleObject(m_hStopEvent, 0) != WAIT_OBJECT_0)
&& (::FindNextFile(hFind, &FindFileData)));
::FindClose(hFind);;
}
}
That's it.
References
- This article on the functionx.com web site describes a Windows Explorer application. I found this article very useful in its straightforward style and I learned a lot about how to work with the Windows Explorer Framework from it. The application isn't, however, anything to do with browsing system files. Instead, it deals with browsing hard coded data.
- I learned about how to get directory listings in 'File and Directory Enumeration' by Andreas Saurwein Franci Gonçalves.
Article History
- 2009-05-04: Updated article
- Hopefully, improved the weak introduction to the article
- Renamed at least one badly named variable
- Provided a description of a function
CLeftView::OpenFolder(...)
that had been omitted
- Rewritten the application as an SDI instead of an MDI (This involves only a slight change in the article and I include instructions on how to create the application as an MDI.)
- Moved the references to the end of the article and tidied them up
- 2006-03-09: Initial article