Prolog: Going Native
When I travel, I don't want to be a tourist so I avoid the tour buses and just explore on my own. When I began to feel like a tourist in "F#",
I decided to go back to "C++". If I used Microsoft's Foundation Classes I could just add a TreeView to my dialog that would populate itself,
and other magic stuff. But that would be kinda touristy too. So I decided to go native.
Exploring (searching and browsing) the file system using Native (Win32 API)
code in C++


To search the file system for file names containing ".cpp", click the "Start Search" button.
To change the search argument, click on the entry field, delete the text, and enter your argument. Do not use
wildcards. Enter either the filename or a part of it. The search begins in the current directory. Files found that
match your argument are added to the listview along with their full path. The current contents of the listview
are not deleted. This allows you to "collect specimens", er... I mean results of multiple searches.
The ListView can be expanded to full screen by resizing the TextBox to nothing then using the drag bar to drag
the ListView to the left edge. Oh!!! There is no drag bar. But it looks like there's a drag bar and it acts like
a drag bar, so, you know what they say: if it smells like a duck and it quacks like a duck - it must be a duck.
We'll just call it a duck -I mean drag bar. Don't tell anybody, okay?


To locate the file and path in the TreeView select a name in the listview
and press the "F3" Function key. To replace the contents of the ListView
select a folder in the TreeView and press "Enter" or double click the
mouse.
To locate the file and path in the TreeView, select a name in the ListView and press the "F3"
Function key. This will climb down the tree to the root and then climb back up to the
folder
for the path and select the TreeView node matching the file name. I put in a
sleep statement
to slow the process down but it is only for visual effect. Without it you might think it
happened instantaneously unless your machine is really slow.
Introduction
As the screen prints above suggest, this article is about searching the file system using only the Win32 API.
Nothing but native code. The search is recursive and uses the FindFirstFile, FindNextFile, FindClose sequence to
read the contents of a directory. The matching logic is the simplest algorithm I could come up with, looking for
a substring in the file names returned. When a match is found the filename and path are added to the ListView.
This is not a wildcard search. If you enter an asterisk as part of the filename it will not find anything. There are
two primary reasons for this approach. The first is to reduce the number of lines of code, the second is, this is
not an example of a search engine nor a sample of regular expressions. The substring method works for me. You can
roll your own if you want to.
The other sample routines included in the program code are necessary to support the search function as part of
the GUI experience (careful there! don't drip that gooey stuff on your keyboard!). I will strive mightily to explain
those things which I struggled with, those things I thought were "cool" and those things that will probably
cause an eyebrow to raise. However I will make no attempt to explain those things that you may feel are corrupt practices.
Background
Why I think this is Beginner level
First, some background on myself. I have been programming for thirty plus years, on various platforms and using various languages.
I first programmed in C++ in ninety two but shortly thereafter, I began to participate more and more in administration and management,
therefore I did less and less programming professionally. I mostly wrote code only when I needed to be creative. Recently I decided
to learn Modern Programming and began exploring other languages. I learned a bit of "F#" and thought it was so cool. I wanted
to share my pleasure. So I contributed some code to CodeProject.
But there were some 'things' I wanted to do that required going outside the language, using "P Invoke" to execute some C/C++
code. Then my explorations revealed to me that I could do functional programming in C++. I really like
F# but I have decided to
learn C++ Functional Programming. There isn't any of that in this article though, this is a purely imperative exploration of C/C++.
Why I want to contribute this article
Now, as for the reason I wanted to share this code, I read a question recently on some forum asking how to make the menu (and other
fonts) in Windows Explorer large enough to be read by someone who doesn't have 20/20 vision. The answer given was to adjust the Desktop Theme. This works well
for XP but with some problems. The other versions of Windows I can't comment on. I did this long ago on my XP but I haven't found a way
to do this on Win7. Possibly because of the built-in magnifier software, it was felt that this is no longer an issue but I find that sometimes
it can be clumsy and frustrating to use anything beyond 100%. I generally provide a larger font or a font dialog because I require it.
This sample code only includes the ability to browse the file system. It is not intended to be used for file management. It can be used
to switch to the real Windows Explorer and open anything that has a file association entry defined for it. To do this, simply add it to
the listview, select it, and either use the "Enter" key on the keyboard or double click it. Depending on the file associations
on your computer, this will open a ".cpp" in Visual Studio, or open a ".sln" in the Visual Studio Start Page, or a
folder in "Windows Explorer" (with 8pt font menus).
Using the code
This solution (.sln) was created by the "New Project..." command on the Visual Studio Start
page. I chose C++ and
Win32 Project, nothing else except Finish. I did not change any Project Properties. I used the #pragma comment to include the
libraries
I needed to include. In the Resource View I added a bitmap resource with six 32 by 32 icons (pardon my
amateurish artwork). You can use
them in any way you wish or replace them if you don't like them. I added a menu and added some buttons to it. I added five dialogs for
the controls I wanted to use. In the code editor I duplicated and changed the wizard generated "About" dialog
proc as a model for each of the five dialogs. The generated code created the main window and the message loop for it. In the
WM_CREATE message handler, I used the CreateDialog function to create the Edit, TreeView, and ListView
child controls and the search dialog popup. In the ListView Proc I used the
DialogBox function to create the Item Removal dialog (delBarProc)
to clean up the Listview. This procedure produced four files that you should either add to your project or replace the generated files with. They
are ".bmp", ".cpp", ".rc" and "Resource.h". If you are
using C++ Express or a batch compile, this should work with no problem. If you have a full version of Visual
Studio you may have to import these files.
Using dialogs, it is quicker and takes less code to create a window but you must handle the additional layer of control. If you
use "CreateWindow()" the control is specified as a Window Style, so you have a control in a window (i.e., WC_TREEVIEW).
But with "CreateDialog()" you have the main window created by the CreateWindow function in InitInstance, then you have
the dialog window created by the CreateDialog function and the TREECTRL, which is also a window, and which you must move and size within the
dialog window procedure whenever the dialog window is moved or sized, unless you are happy with the default movement and sizing. This
approach has the advantage of separating the code for the controls into separate procedures, making it very easy to place the entire
procedure, unchanged, into another project, while changing everything else that it interacts with.
A word about the bitmap. It is twice as wide and twice as tall as usual. If you want to use a smaller image you need to change the
first two parameters for the Imagelist_Create function from 32 to 16. These parameters also control the size of the
buttons for the TreeView. Make the image smaller and the buttons get smaller. Also, the images are folder closed, folder closed and selected, folder
opened, folder opened and selected, non-folder, and non-folder selected. The selected version of the images
are lighter in color than the non-selected
versions so that an item is kind of highlighted when it is the "selected" item.
The system handles this change using the iImage and
iSelectedImage attributes of the tvitem. It's not so easy with folder closed and folder opened however. This must be handled in code which
we do in the treeview proc, in the WM_NOTIFY message handler. It is not functionally necessary to handle this situation but I use the value of the
iImage attribute to control the flow of program logic in some cases.
Explaining the code
In the first section of code below, you will see mostly the code generated from the
wizard. I did add more global variables and additional forward
declarations. I added the forward declarations in the order in which I want to explain them to reduce the amount of jumping around in the
article.
I rearranged the routines so they would be in the same order as the declarations. This is not a language requirement. Note the #pragma comments.
The libraries were included in this way so that nothing needs to be added to the project properties. This gave me a clean compile on a Win7
machine with a full version of Visual Studio and an XP SP3 machine with C++ Express. I used the files from the Win7 version on the XP, simply
replacing the generated files and copying in the bitmap.
Notice in the code below, I have emphasized two lines of code. Both lines load a cursor. The first one is at the global level. This could
be moved to the the WndProc routine but I didn't know where it would be used. The IDC_ARROW is the class cursor but you need to change it to
IDC_SIZEWE (that's west, east I presume) to indicate that the edges of the two windows can be moved, simulating a grab bar.
The program code:
#include "stdafx.h"
#include "natSyncSearch.h"
#include <commctrl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <Shellapi.h>
#pragma comment(lib, "User32.lib")
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "Shell32.lib")
#define MAX_LOADSTRING 100
#define BUFSIZE 512
HINSTANCE hInst; TCHAR szTitle[MAX_LOADSTRING]; TCHAR szWindowClass[MAX_LOADSTRING]; HWND hWnd, hEdit, hTree, hList, hAbout, hToolBar, hdelBar, hFfext, hFormView, hTreeView, hListView;
TCHAR driveRoot[MAX_PATH], searchStr[MAX_PATH]; HTREEITEM Selected, driveParent;
TV_ITEM tvi, tvi2;
static int tviiImage = 4; static int tviiSelectedImage = 5; TCHAR startInDir[256];
wchar_t *sId = L"C:\\Users"; int sidLen;
TCHAR saveCD[256];
TCHAR nodeCD[MAX_PATH]; HTREEITEM nodeParent;
HIMAGELIST hImageList;
HBITMAP hBitMap;
static BOOL shownYet = false;
HCURSOR hCursor = LoadCursor(NULL, IDC_SIZEWE);
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int recursivefileSearch(TCHAR *name, bool hidden = false);
INT_PTR CALLBACK toolBarProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
INT_PTR CALLBACK editBoxProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
INT_PTR CALLBACK treeViewProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
INT_PTR CALLBACK listViewProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
bool SetTreeviewImagelist(const HWND hTv);
BOOL InitTreeViewItems(HWND hwndTV);
void getDirectories(HTREEITEM hDir, LPTSTR lpszItem);
HTREEITEM AddItemToTree(HWND hwndTV, HTREEITEM hDir, LPTSTR lpszItem);
BOOL getNodeFullPath(HWND thisHwnd, HTREEITEM nmtvi2);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
MSG msg;
HACCEL hAccelTable;
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_NATSYNCSEARCH, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_NATSYNCSEARCH));
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg) ||
!IsDialogMessage(msg.hwnd, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_NATSYNCSEARCH));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_HOTLIGHT);
wcex.lpszMenuName = MAKEINTRESOURCE(IDC_NATSYNCSEARCH);
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
return RegisterClassEx(&wcex);
}
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance;
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
WndProc: The Window Procedure!
WM_CREATE: Give it some children!
WndProc is the message handler for the main window. Here the messages are handled in place. They could easily be separated into individual
functions for each message. This way is easier to explain though. The first message,
WM_CREATE is where the child amd popup windows are created
whether you use "CreateDialog" or "CreateWindow". The important difference is using
CreateWindow means you have to handle all of the messages in WndProc, while using
CreateDialog enables you to separate the message handling
into separate procedures for each control without needing to sub-class the controls. You can, therefore, put a large portion of the code for
a control in its own procedure, making it easy to quickly combine any number of controls into a unique User Interface.
Here's how it works - using CreateWindow you cast the control's ID,
IDC_TREE1, for example to an HMENU for the Menu parameter
of the CreateWindow function call (that's the NULL parameter before the
hInstance parameter in the InitInstance CreateWindow
above.). Then you handle the notifications sent by the control in the WM_NOTIFY message of
WndProc or WM_COMMAND message for some controls. If
you have multiple controls you will need multiple switches in WM_NOTIFY to handle each control separately, as well as a switch within that
switch to handle the notifications sent by the control.
Using CreateDialog you MAKEINTRESOURCE of the dialog
IDD_FORMVIEW1 you defined in the Dialog Editor, specifying NULL for
the GetModuleHandle argument in the first parameter (which means the resource you want to load is in the current load module), the
parent window's handle, hWnd, and the DLGPROC (treeViewProc, to process the messages sent by the control) and name the
handle to the window hTreeView. If the CreateDialog function
succeeded you use GetDialogItem to get the control in the hTreeView
window, IDC_TREE1 and name it hTree. Then you show the window, like this:
hTreeView = CreateDialog(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_FORMVIEW1),hWnd,treeViewProc);
if (hTreeView != NULL) { hTree = GetDlgItem(hTreeView, IDC_TREE1); ShowWindow(hTreeView, SW_SHOW); }
Of course this is only for the TreeView control, but it's pretty much the same for the other children. As for the
popups, I created one
in the WM_CREATE message, the hToolBar, because I want it to be the first window with focus. Later I may move it to the WM_COMMAND message handler so the
user can invoke it from the menu. The "Show" and "Hide" menu items aren't really necessary. They're just there for show.
We also need to get the Client Rectangle for the main window, hWnd,
and use rect.right to get the width of the main window. The height
doesn't really matter for now. We just want to set the height and width of the edit control and set the listview's left edge a little farther
to the right relative to the right edge of the treeview and set them both a little lower than the bottom of the edit control. All three windows
will be sized properly in the WM_SIZE notification for the main window. The controls are sized in the WM_SIZE for the control's parent. This additional
layer of control is a small inconvenience for the simplicity it provides.
We also set the treeview imagelist and set the current directory to "sId", which is "C:\Users", a folder that exists on
Win7 but not on XP, unless you have created one with that name. The SetCurrentDirectory function will set the current directory if that folder
exists. We get the current directory and compare it with "sId", ignoring case (i.e., a==A). If they are not equal we set the current
directory to a folder that should exist on XP but not on Win7. If we did not set this value, the
Search dialog would start searching in the
folder containing the load module. That's the value of the current directory when the program starts and is the value used as the starting point.
We also must initialize the treeview, which we call a function named InitTreeViewItems to accomplish. Now the Main
window is all set up.
We could end WM_CREATE at this point and break, but the user might not realize the purpose of the
article, which is the "Search" function.
So, we create the hToolBar popup and put it up above the window title
bar so that it does not cover anything in the window displayed. We can place it
anywhere because we specified POPUP instead of CHILD for the dialog
style. A hint is provided to inform the user the search argument can be changed. The
user is not forced to start the search immediately. The program responds to user actions in a fashion that Windows Explorer
users will probably feel
comfortable with. The user can exit the program by clicking on the close button (X) on the Title Bar or ALT+F4. This is to retain intuitive essence.
The window procedure code
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
case WM_CREATE:
{
RECT lrccl, crccl, drccl;
hFormView = CreateDialog(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDD_FORMVIEW),hWnd,editBoxProc);
if (hFormView != NULL)
{
hEdit = GetDlgItem(hFormView, IDC_EDIT1);
SetWindowText(hEdit, L"Enter EXTENSION or PART of filename.");
ShowWindow(hFormView, SW_SHOW);
}
SetWindowText(hEdit, L"Default search string is '.cpp'! Change it above.");
hTreeView = CreateDialog(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDD_FORMVIEW1),hWnd,treeViewProc);
if (hTreeView != NULL)
{
hTree = GetDlgItem(hTreeView, IDC_TREE1);
ShowWindow(hTreeView, SW_SHOW);
}
hListView = CreateDialog(GetModuleHandle(NULL),
MAKEINTRESOURCE(IDD_FORMVIEW2),hWnd,listViewProc);
if (hListView != NULL)
{
hList = GetDlgItem(hListView, IDC_LIST1);
ShowWindow(hListView, SW_SHOW);
}
GetClientRect(hWnd,&crccl);
MoveWindow(hFormView,0,0,crccl.right,50,true);
MoveWindow(hTreeView,0,60,675,490,true);
MoveWindow(hListView,685,60,305,490,true);
SetTreeviewImagelist(hTree);
SetCurrentDirectory(sId);
GetCurrentDirectory(255, startInDir);
if (_wcsicmp(startInDir, sId)!=0)
{
wsprintf(nodeCD,L"%s", L"C:\\Documents and Settings"); SetCurrentDirectory(nodeCD);
GetCurrentDirectory(255, startInDir);
}
else
{
wsprintf(nodeCD,L"%s", sId); }
InitTreeViewItems(hTree);
hToolBar = CreateDialog(GetModuleHandle(NULL),MAKEINTRESOURCE(IDD_DIALOGBAR),hWnd,toolBarProc);
if (hToolBar != NULL)
{
hFfext = GetDlgItem(hToolBar, IDC_EDIT1);
SetWindowText(hFfext, L"Enter EXTENSION or PART of filename.");
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hToolBar,&drccl);
if (lrccl.top<40)
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top+36,crccl.right,50,true);
}
else
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top-36,crccl.right,50,true);
}
ShowWindow(hToolBar, SW_SHOW);
}
SetWindowText(hEdit, L"Default search string is '.cpp'! Change it above.");
SetFocus(hTree);
return 0;
}break;
WM_COMMAND: Would you like to see a menu?
This is the place where menu commands and messages from older controls that are not "Common Controls" are processed. If you create
child windows instead of child dialogs that have these older controls, those messages are processed here but the "Common Controls"
are processed in the WM_NOTIFY switch. Since the Search dialog can be
hidden or destroyed by user action, we will either Show or Create It here. The
processing of the Search dialog is handled in the toolBarProc specified in the DLGPROC parameter of the CreateDialog
function. In the case of IDM_ABOUT, the About Box, the DialogBox function is called to create a modal dialog box, which means control is passed to the DlGPROC and you
can't do anything until you end the dialog. Notice the 'IDM_' in IDM_ABOUT and IDM_EXIT, contrasted with 'ID_' in ID_SEARCH. This 'IDM_'
indicates that IDM_ABOUT is a popup sub-item on the Menu Bar. The 'ID_' indicates that ID_SEARCH is an action button on the menu, not a popup sub-item.
The WM_COMMAND code
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case ID_SEARCH:
case ID_SHOW:
{
RECT lrccl, crccl, drccl;
if (hToolBar != NULL)
{
hFfext = GetDlgItem(hToolBar, IDC_EDIT1);
SetWindowText(hEdit, L"Enter EXTENSION or PART of filename in Text Box aboce.");
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hToolBar,&drccl);
if (lrccl.top<40)
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top+36,crccl.right,50,true);
}
else
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top-36,crccl.right,50,true);
}
ShowWindow(hToolBar, SW_SHOW);
}
else
{
hToolBar = CreateDialog(GetModuleHandle(NULL),MAKEINTRESOURCE(IDD_DIALOGBAR),hWnd,toolBarProc);
if (hToolBar != NULL)
{
SetWindowText(hEdit, L"Enter EXTENSION or PART of filename in Text Box above.");
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hToolBar,&drccl);
if (lrccl.top<40)
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top+36,crccl.right,50,true);
}
else
{
MoveWindow(hToolBar,lrccl.left+8,lrccl.top-36,crccl.right,50,true);
}
ShowWindow(hToolBar, SW_SHOW);
}
}
return 0;
}
break;
case ID_HIDE:
ShowWindow(hToolBar, SW_HIDE);
ShowWindow(hdelBar, SW_HIDE);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
The imaginary splitter bar: Doctor, I'm seeing things that aren't there!
Has your older brother ever held his index finger a quarter of an inch from your eye and proclaimed "I'm not touching
you!".
You try to knock his hand away but he just moves it out of your reach and goes to your other eye. You instinctively dodge his finger but he's
faster than you. You know that sooner or later he is going to poke you in the eye unless you close your eyes. When you open them again, you discover
that your last piece of candy is gone but he denies any knowledge of your candy. That never happened to me, but
okay, now replace his index
finger with the mouse pointer. Your eyes become the windows containing the tree control and list control. When the finger, I mean, Mouse Pointer, moves (WM_MOUSEMOVE), the window is moved to keep the pointer away from it.
Of course, we only do this if WM_LBUTTONDOWN (where we set capture). We
detect left button down with wParam being equal to MK_LBUTTON. We set the cursor before we test
wParam so the cursor is the west-east cursor when
the splitter bar effect can be activated. We use the GetCursorPos function to get the position of the pointer and the GetClientRect and
GetWindowRect functions to determine which way and how far to move each window. As the windows are moved they are also re-sized in the WM_SIZE
notification. This process continues until the left button is released, at which point we process a WM_LBUTTONUP message and release the mouse
capture. This code moves and sizes the dialog windows, not the controls. They are sized in the DLGPROC of the respective containing dialogs. All
of the other messages use the default processing as provided by the Project Creation Wizard.
The (non-)imaginary Splitter Bar code
case WM_LBUTTONUP:
ReleaseCapture();
break;
case WM_LBUTTONDOWN:
SetCapture(hWnd);
break;
case WM_MOUSEMOVE:
{
int rcoff;
POINT spt, cpt;
POINT sptc, cptc;
RECT erc, lrc, trc, wrc ;
RECT erccl, lrccl, trccl, wrccl ;
GetCursorPos(&spt);
cpt.x = (short)LOWORD(lParam); cpt.y = (short)HIWORD(lParam); cptc=cpt;sptc=spt;
ClientToScreen(hWnd, &sptc);
ClientToScreen(hWnd, &cptc);
GetClientRect(hWnd,&wrccl);
GetWindowRect(hWnd,&wrc);
GetClientRect(hFormView,&erccl);
GetWindowRect(hFormView,&erc);
if (erccl.bottom < 20)
{
erc.bottom = erc.bottom + 20;
}
rcoff=erc.top-wrc.top;
GetClientRect(hTreeView,&trccl);
GetWindowRect(hTreeView,&trc);
GetClientRect(hListView,&lrccl);
GetWindowRect(hListView,&lrc);
if (spt.y > trc.top)
{
SetCursor(hCursor);
}
if (wParam == MK_LBUTTON)
{
if (spt.y > erc.bottom)
if (spt.x > trc.right || spt.x < lrc.left)
{
sptc.x = sptc.x-wrc.left;
OffsetRect(&erc, -wrc.left, -wrc.top);
OffsetRect(&trc, -wrc.left, -wrc.top);
OffsetRect(&lrc, -wrc.left, -wrc.top);
MoveWindow(hFormView,0,erc.top-rcoff,wrccl.right,erc.bottom-rcoff,true);
MoveWindow(hTreeView,0,erc.bottom-rcoff+4,cpt.x-4,wrccl.bottom-erc.bottom+rcoff,true);
MoveWindow(hListView,cpt.x+4,erc.bottom-rcoff+4,wrccl.right-cpt.x-4,wrccl.bottom-erc.bottom+rcoff,true);
UpdateWindow(hWnd);
}
}
return 0;
}break;
case WM_SIZE:
{
int rcoff;
RECT erc, lrc, trc, wrc ;
RECT erccl, trccl, wrccl ;
int cptx = (short)LOWORD(lParam); int cpty = (short)HIWORD(lParam); GetClientRect(hWnd,&wrccl);
GetWindowRect(hWnd,&wrc);
erccl=wrc;
OffsetRect(&erccl, -wrc.left, -wrc.top);
GetWindowRect(hFormView,&erc);
rcoff=erc.top-wrc.top;
GetClientRect(hTreeView,&trccl);
GetWindowRect(hTreeView,&trc);
GetWindowRect(hListView,&lrc);
OffsetRect(&erc, -wrc.left, -wrc.top);
OffsetRect(&trc, -wrc.left, -wrc.top);
OffsetRect(&lrc, -wrc.left, -wrc.top);
MoveWindow(hFormView,0,erc.top-rcoff,wrccl.right,erc.bottom-rcoff,true);
MoveWindow(hTreeView,0,erc.bottom-rcoff+4,trc.right-trc.left,cpty-trc.top+rcoff+4,true);
MoveWindow(hListView,lrc.left-trc.left,erc.bottom-rcoff+4,cptx-lrc.left+8,cpty-trc.top+rcoff+4,true);
return 0;
}break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
The search function
An imperative for exploration
The File Management function FindFirstFile searches a directory supplied by your code for a file or subdirectory with a name that matches the
name you supply (or partial name if wildcards are used). It searches only the directory with the name you supply. If you want to include all files and sub
directories you simply use "dirName\*". You also supply a buffer mapped by a WIN32_FIND_DATA structure to contain the information returned.
It returns a search handle that can be used in a call to FindNextFile or FindClose. We use it in two different ways. The first is to get the
files and sub-directories for a single directory in each call. In this call we populate that treenode. This is in a function named getDirectories. It is
called from InitTreeviewItems as well as the TVN_ITEMEXPANDING notification.
In the second use of the FindFirstFile triad, we look for file-names that contain the specified sub-string and add them to the ListView or if they
are sub-directories we call the search function recursively to search them. We use the file attribute FILE_ATTRIBUTE_HIDDEN to filter out hidden
directories and files. In a recursive loop this makes a very noticeable difference in speed. You probably don't want to see those files anyway, so
why waste the time looking for them?
I should also mention my usage of wsprintf as a replacement for
wcscpy and wcscat combinations. Although wsprintf may be slower than
wcscpy/wcscat, I did not perceive any change in the speed while changing from one to the other, in either direction. The
wsprintf actually eliminated a
large number of Security Warnings. In this usage we look at the next to last character of the name that is passed to us. When it is a backslash we add an
asterisk, otherwise we add a backslash and asterisk to set up for FindFirstFile. This is equivalent to a
wcscpy followed by a wcscat followed by another
wcscat. In setting up to make the recursive call we also look at the next to last character, add either a backslash and the sub directory or just the sub
directory. I placed the recursive call last in the nested 'If' statement which caused a match on a directory to be found. This would prevent that sub
directory from being searched. To prevent this I used the FILE_ATTRIBUTE_DIRECTORY file attribute to skip the matching logic on a directory and again to
prevent a recursive call on a file. The match logic itself is just a wcsstr function looking for "searchStr"
in "ffd.cFileName". If you want a different matching routine this is where you do it. When we have a match we insert the name
into the ListView with zero as the iItem index value and a iSubItem column index value. Since the ListView is sorted in ascending order the
iItem is inserted
where it belongs, returning the value to be used to set any iSubItem attribute value. We use it to set the
pszText attribute for iSubItem (path column).
Any column you add, you set their value here. All of the attributes are available at this point but there are only two columns for the filename and path.
The recursive file search code
int recursivefileSearch(TCHAR *name, bool hidden){
WIN32_FIND_DATA ffd;
HANDLE hFind = INVALID_HANDLE_VALUE;
DWORD dwError=0;
TCHAR recursiveSearchPath[MAX_PATH];
if(name[wcslen(name)-1]!=L'\\')
{ wsprintf(recursiveSearchPath,L"%s\\*", name);
}
else
{
wsprintf(recursiveSearchPath,L"%s*", name);
}
hFind = FindFirstFile(recursiveSearchPath, &ffd);
do { if (hFind == INVALID_HANDLE_VALUE) return -1; if ((wcscmp(ffd.cFileName, L"."))== 0 || (wcscmp(ffd.cFileName, L".."))== 0)
{;} else if (ffd.dwFileAttributes
& FILE_ATTRIBUTE_HIDDEN
&& !hidden)
{;} else if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
&& (wcsstr(ffd.cFileName, searchStr)) )
{ LVITEM item;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iItem = 0;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = ffd.cFileName;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret;
item.iSubItem = 1;
item.pszText = name;
ListView_SetItem(hList, &item);
}
else if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{ if(name[wcslen(name)-1]!=L'\\')
{
wsprintf(recursiveSearchPath,L"%s\\%s", name, ffd.cFileName);
}
else
{
wsprintf(recursiveSearchPath,L"%s%s", name, ffd.cFileName);
}
recursivefileSearch(recursiveSearchPath, hidden); }
}
while (FindNextFile(hFind, &ffd) != 0);
dwError = GetLastError();
if (dwError != ERROR_NO_MORE_FILES)
if (dwError != ERROR_ACCESS_DENIED)
{
SetWindowText(hEdit, L"Error in Find File Routine !");
}
FindClose(hFind);
return dwError;
}
In support of the Search function: The toolBarProc
I used a ToolBar dialog as a starting point for this function but I don't know that it was the best choice. It probably has features that I didn't use.
It just worked okay and I had no reason to change it. It's pretty simple. There is a Button and an Edit
control. You can enter a sub-string to search
for or click the button to start the search. The button gets the handle for the item in the
tree that is highlighted and finds its full path. Then it
invokes the search function and hides the dialogbox for later use. It can be destroyed by clicking anywhere in the dialog except the
button and then
pressing Alt + F4. Pressing this combination twice or without first clicking on the dialog cause an Exit from the program. Clicking on the Edit
Control when the Hint is still showing will change the contents to the default search string. This is done when you click the button also. If you
want to change the default you should change it in both places unless you specifically want to have alternate defaults.
The code for toolBarProc
INT_PTR CALLBACK toolBarProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BUTTON1)
{
hFfext = GetDlgItem(hDlg, IDC_EDIT1);
GetWindowText(hFfext, searchStr, MAX_PATH);
if(wcscmp(searchStr, L"Enter EXTENSION or PART of filename.")==0)
{
wsprintf(searchStr,L"%s", L".cpp");
SetWindowText(hFfext, searchStr);
}
tvi.hItem = TreeView_GetDropHilight(hTree);
getNodeFullPath(hTree, tvi.hItem);
recursivefileSearch(nodeCD, false); ShowWindow(hToolBar, SW_HIDE);
}
if (LOWORD(wParam) == IDC_EDIT1)
{
if (HIWORD(wParam) == EN_SETFOCUS)
{ hFfext = GetDlgItem(hDlg, IDC_EDIT1);
GetWindowText(hFfext, searchStr, MAX_PATH);
if(wcscmp(searchStr, L"Enter EXTENSION or PART of filename.")==0)
{
wsprintf(searchStr,L"%s", L".cpp");
SetWindowText(hFfext, searchStr);
}
}
}
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
Cleaning up the ListView: The delBarProc
This popup DialogBar is created in the listViewProc by right clicking on an item or pressing the delete key. It allows you to delete listview items only. It does not delete files from the file system. You can delete the selected item, all items of the same type, all items with the same
path, or everything. With this you can collect groups of files such as cpp's, headers, and sln's, or any grouping you choose.
You can also add individual files from the tree to the ListView by clicking them in the TreeView or moving the caret to them with the cursor keys and then
pressing Enter. Doing either of these to a folder will clear the contents of the ListView and
insert the contents of the folder in their place. The point
of this is that navigating the tree should be intuitive for a Windows user, therefore anything the user does should result in an action the user
understands. Clicking on a folder in the Windows Explorer TreeView pane produces the same results. Here, we include files in the treeview and clicking
on them has a similar effect. Even though it doesn't delete anything from the listview, it is not so different as to be counter-intuitive.
From this discussion it is obvious that a separate function to remove items from the lisview is not an absolute necessity. It is provided to
selectively control the contents of the listview rather than simply deleting everything and then
putting something else in its place.
The code for the delBarProc
INT_PTR CALLBACK delBarProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BUTTON1)
{
LVITEM item;
TCHAR Text[256]={0};
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = Text;
int ret = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
while (ret> -1)
{
ListView_DeleteItem(hList, ret);
ret = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
}
ShowWindow(hdelBar, SW_HIDE);
}
if (LOWORD(wParam) == IDC_BUTTON2)
{
LVITEM item;
TCHAR Text[256]={0};
TCHAR extPat[255]={0};
TCHAR * pdest;
TCHAR * ppatt;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 0;
item.cchTextMax = 255;
item.pszText = Text;
int ret = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
if(ret != -1)
{
item.iItem = ret;
ListView_GetItem(hList,&item);
}
ppatt = extPat;
pdest = wcsrchr(Text,L'.');
while (*pdest)
{
*ppatt++ = *pdest++;
}
ret = ListView_GetNextItem(hList,-1,LVNI_ALL);
memset(&Text,0,sizeof(Text));
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 0;
item.cchTextMax = 255;
item.pszText = Text;
item.iItem = ret;
ListView_GetItem(hList,&item);
while (ret> -1)
{
if (wcsstr(Text,extPat))
ListView_DeleteItem(hList, ret--);
ret = ListView_GetNextItem(hList,ret,LVNI_ALL);
memset(&Text,0,sizeof(Text));
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 0;
item.cchTextMax = 255;
item.pszText = Text;
item.iItem = ret;
ListView_GetItem(hList,&item);
}
ShowWindow(hdelBar, SW_HIDE);
}
if (LOWORD(wParam) == IDC_BUTTON3)
{
LVITEM item;
TCHAR Text[256]={0};
TCHAR pathPatt[256]={0};
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 1;
item.cchTextMax = 260;
item.pszText = Text;
int ret = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
item.iItem = ret;
ListView_GetItem(hList,&item);
wsprintf(pathPatt,L"%s", item.pszText);
memset(&Text,0,sizeof(Text));
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 1;
item.cchTextMax = 255;
item.pszText = Text;
ret = ListView_GetNextItem(hList,-1,LVNI_ALL);
item.iItem = ret;
ListView_GetItem(hList,&item);
while (ret> -1)
{
if (_wcsicmp(Text,pathPatt)==0)
ListView_DeleteItem(hList, ret--);
ret = ListView_GetNextItem(hList,ret,LVNI_ALL);
memset(&Text,0,sizeof(Text));
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iSubItem = 1;
item.cchTextMax = 255;
item.pszText = Text;
item.iItem = ret;
ListView_GetItem(hList,&item);
}
ShowWindow(hdelBar, SW_HIDE);
}
if (LOWORD(wParam) == IDC_BUTTON4)
{
ListView_DeleteAllItems(hList);
ShowWindow(hdelBar, SW_HIDE);
}
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
The dialog procedure for the Edit Control
The Edit Control is intended to provide information to the user. It is a passive control in this version, meaning it doesn't need a DLGPROC. It
is necessary for any functionality you might want to add to the Edit Control, to make it an active control. If you want to save a few lines of code
you could use NULL for the DlGPROC parameter. This has the same effect as specifying
NULL for the Menu parameter of the CreateDialog function. It isn't
necessary for the functionality we are using now but if you want to add anything, you will need it. Otherwise the procedure is just a place holder.
The code for the editBoxProc
INT_PTR CALLBACK editBoxProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
The dialog procedure for the TreeView
I begin the discussion of the treeViewProc with the WM_INITDIALOG message. In this instance no code was added for this message because the setup was
done in the main window procedure, WndProc. This is where you could call the
InitTreeViewItems function and the SetTreeviewImagelist function. But if you are
coding a CreateWindow version, you don't have a WM_INITDIALOG message, so you call them from the
WM_CREATE message of the WndProc.
Going on to the WM_NOTIFY message; the first switch statement selects the idFrom member of the NMHDR. In a Dialog Proc we do not need to do
this because we know that this is the treeview control. But in the CreateWindow version we do need to do this because we need to select the other
controls in the main window. You could put this WM_NOTIFY, unchanged, into the
WndProc procedure of a CreateWindow version, along
with the switches you need for any other control you want to put in the main window. To use the listViewProc for this, you must add the case for IDC_LIST1 to the
switch.
TVN_SELCHANGING: Keeping it synchronized
The TVN_SELCHANGING notification is the first message the treeview control sends when the user clicks an item or moves the cursor to an item. The
only action we need to take is to find the full path for the item represented by the item text. Failure is not an option. If it fails, the tree will not
function properly. A possible way to find the full path is to climb down the tree, taking names and kicking bu--, oh no, that's something else I'm
doing. For this function, I'm just collecting names in a sub-routine called
getNodeFullPath. This is a separate function because it is used in more than
one place. The TVN_ITEMEXPANDING notification can be sent by clicking on the plus button of an item that has not been selected. In this case the
node's
full path is not known. To detect this situation we must get the selected item to determine if it is the one we are expanding. If it isn't we have to get its
full path so we can populate its children if there are any. This doesn't select the item and can break the search function. Therefore we need to get the
highlighted item and determine its full path. Let's look at WM_INITDIALOG and the TVN_SELCHANGING
notification of WM_NOTIFY before continuing.
The code for WM_INITDIALOG, WM_NOTIFY - TVN_SELCHANGING
INT_PTR CALLBACK treeViewProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_NOTIFY:
{ switch (((LPNMHDR)lParam)->idFrom)
{
case IDC_TREE1:
{
switch (((LPNMHDR)lParam)->code)
{
case TVN_SELCHANGING: { NMTREEVIEW * pnmtv; pnmtv = (LPNMTREEVIEW)lParam; tvi=((pnmtv)->itemNew); getNodeFullPath(hTree, tvi.hItem);
return false;
} break;
TVN_SELCHANGED: How did we get here?
The TVN_SELCHANGED notification processes three actions: TVC_UNKNOWN is caused by code internal to the control or code external to the control, that is,
program code. The action we need to perform here is to discover if this item's text is contained in the start in directory when concatenated to the current
directory. When this is so we cause this node to be expanded and focused on until we discover the two are equal, then we blank out the start in directory.
This process can be started at program start-up by the InitTreeviewItems function where it is the find start in directory function or by user action in the
listViewProc function in response to pressing the F3 key after having selected an item. This is the 'Locate File in Tree' function. Let's look at the
TVC_UNKNOWN action code of the TVN_SELCHANGED notification processed by WM_NOTIFY before going on to the other two action codes.
The code for WM_NOTIFY - TVN_SELCHANGED -- TVC_UNKNOWN
case TVN_SELCHANGED:
{ NMTREEVIEW * pnmtv;
pnmtv = (LPNMTREEVIEW)lParam;
HTREEITEM nmtvi;
nmtvi = TreeView_GetSelection(hTree);
TCHAR Text[256]={};
TCHAR text[256]={};
int result = 0;
memset(&tvi,0,sizeof(tvi));
tvi.mask = TVIF_TEXT | TVIF_CHILDREN| TVIF_IMAGE;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if((pnmtv)->action==TVC_UNKNOWN)
{
if (*startInDir)
{ if (_wcsnicmp(startInDir, nodeCD, wcslen(nodeCD)) == 0 )
{ if(TreeView_GetItem(hTree, &tvi))
{ if (tvi.cChildren > 0 )
{ if (tvi.state & TVIS_EXPANDEDONCE)
{ nmtvi=TreeView_GetChild(hTree, nmtvi);
while (nmtvi){ memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT | TVIF_CHILDREN| TVIF_IMAGE;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if(TreeView_GetItem(hTree, &tvi))
{
if(nodeCD[wcslen(nodeCD) - 1] != L'\\')
{
wsprintf(text, L"%s\\%s", nodeCD,tvi.pszText); }
else
{
wsprintf(text, L"%s%s", nodeCD,tvi.pszText); }
result = wcslen(text);
if (sidLen == result && (_wcsicmp(startInDir, text)== 0)) { SetCurrentDirectory(nodeCD);
memset(startInDir,0,sizeof(startInDir));
TreeView_Select(hTree,nmtvi,TVGN_CARET);
TreeView_Select(hTree,nmtvi,TVGN_DROPHILITE);
TreeView_Select(hTree,nmtvi,TVGN_FIRSTVISIBLE); ReleaseCapture();
SetCapture(hList);
SetFocus(hTree);
ReleaseCapture();
return 0;
}
else
{
wsprintf(text, L"%s\\", text); // concat a back-slash
result = wcslen(text); //take the length again(it just changed)
if (_wcsnicmp(startInDir, text, result) == 0 )
{ // is folder in path
if (tvi.cChildren > 0 )
{
if (!(tvi.state & TVIS_EXPANDEDONCE ))
{ //node has children but has never been expanded. TVN_EXPANDED will take over now.
TreeView_Expand(hTree, tvi.hItem, TVE_EXPAND); // we are through here
return 0; //terminate the routine
}
else
{ // folder in path, go on to the next higher level
if(nodeCD[wcslen(nodeCD) - 1] != L'\\')
{
wsprintf(nodeCD,L"%s\\%s", nodeCD,tvi.pszText);
}
else
{
wsprintf(nodeCD,L"%s%s", nodeCD,tvi.pszText);
}
nmtvi=TreeView_GetChild(hTree, nmtvi);
}
}
else
{ // there are no children. end
return 0;
}
}
else
{
nmtvi = TreeView_GetNextSibling(hTree,nmtvi); // get the next child's handle
}
}
}
} // loop to get the child's label text
} // node has children and has been expanded. go on to the next higher level
} // does it have children
} // could you get the item
} // is it in the path
else
{
return 0; // don't do anything
}
return 0;
} // is there a start-in-directory
}
WM_NOTIFY - TVN_SELCHANGED -- By keyboard or by mouse
TVC_BYKEYBOARD results from the user pressing the up or down cursor keys, moving the selection and focus. Since multiple items can be selected we must
make it so by programmatically selecting and highlighting the item.
TVC_BYMOUSE indicates the user has selected the item by clicking on it. If the item isn't a folder we want to insert it into the listview, scrolling to
it, and selecting it. On my XP the item showed as selected but on Win7 it did not, so I put in a sleep function call so it would show as high-lighted for
one second. When it woke up the highlight went off until you moved focus to the ListView
control. If the item was a folder we clear the listview and get
the children of the folder item, placing them into the listview in a do loop, repeating until we run out of children. Let's look at these two action codes.
The code for WM_NOTIFY - TVN_SELCHANGED -- TVC_BYKEYBOARD and TVC_BYMOUSE
if((pnmtv)->action==TVC_BYKEYBOARD)
{
TreeView_Select(hTree,nmtvi,TVGN_CARET);
TreeView_Select(hTree,nmtvi,TVGN_DROPHILITE);
return 0;
}
if((pnmtv)->action==TVC_BYMOUSE)
{
TreeView_Select(hTree,nmtvi,TVGN_DROPHILITE);
TreeView_Select(hTree,nmtvi,TVGN_CARET);
tvi.hItem = nmtvi;
if(TreeView_GetItem(hTree, &tvi))
{
if (tvi.iImage == 4)
{ LVITEM item;
memset(&item,0,sizeof(item));
GetCurrentDirectory(MAX_PATH,nodeCD);
item.mask = LVIF_TEXT;
item.iItem = 0;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = tvi.pszText;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret;
item.iSubItem = 1;
item.pszText = nodeCD;
ListView_SetItem(hList, &item);
int nRet = ret+1;
ListView_GetItemText(hList,nRet,0,text,255);
while (_wcsicmp(text, tvi.pszText)==0)
{ ListView_GetItemText(hList,nRet,1,text,255);
if (_wcsicmp(text, nodeCD)==0)
{ ListView_DeleteItem(hList,ret); ret = nRet;
}
nRet = nRet+1;
ListView_GetItemText(hList,nRet,0,text,255); }
ListView_SetItemState(hList,-1,0,LVIS_SELECTED); ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_DROPHILITED,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_SELECTED,LVIS_SELECTED);
ListView_SetItemState(hList,ret,LVIS_FOCUSED,LVIS_FOCUSED);
ListView_EnsureVisible(hList,ret, true);
ReleaseCapture();
SetCapture(hList);
SetFocus(hList);
Sleep(1000);
ReleaseCapture();
} else if (tvi.cChildren == 1)
{ nmtvi=TreeView_GetChild(hTree, nmtvi);
memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT | TVIF_CHILDREN| TVIF_IMAGE;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if(TreeView_GetItem(hTree, &tvi))
{
ListView_DeleteAllItems(hList);
GetCurrentDirectory(MAX_PATH,nodeCD);
do
{
LVITEM item;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iItem = 0;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = tvi.pszText;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret;
item.iSubItem = 1;
item.pszText = nodeCD;
ListView_SetItem(hList, &item);
if (ret < 1)
ret *= -1;
nmtvi = tvi.hItem;
nmtvi = TreeView_GetNextSibling(hTree,nmtvi);
memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
} while (TreeView_GetItem(hTree, &tvi));
}
}
}
}
} break;
WM_NOTIFY - TVN_ITEMEXPANDING: The key to the Castle
The TVN_ITEMEXPANDING notification is sent by the tree control before expanding a node. This is the
key to keeping the tree sparsely populated while
making it appear to be fully populated. We need to determine if the tree is performing the TVE_COLLAPSE action. If so we set the
iImage and iSelectedImage to
closed-folder and closed-folder-selected, then exit, otherwise we set the indexes to opened-folder and opened-folder-selected. Then we check to see if the
folder was opened previously (TVIS_EXPANDEDONCE). If this is the case we don't want to add duplicates so we exit here also. Now we have the case of a user
clicking the button of a folder item that is not selected. We don't know when this might happen so we test for the situation before attempting to populate
the node's grandchildren. To detect it we use the TreeView_GetSelection macro and compare the returned value to
NULL and then to the itemNew.hItem member
of the NMTREEVIEW structure. Since the itemOld member isn't used by
the TVN_ITEMEXPANDING notification it has no information we can use. If the value is
not NULL and the two values are not the same we know the button was clicked so we discard the value and use
itemNew to get the full path if we want speed or
with the TreeView_SelectItem macro if we want to see the folders expanding on the screen. I just left them both in but either will work alone. Before we
release the node for expansion we give it grand children, that is, we populate its children with their children so that when the node is expanded
its children will have their buttons!, indicating that they have some children. Any children without buttons are sterile and have no children of their
own. This is one way to create a sparse tree effect but it is not the only way. One item to research on this subject is the TVN_GETDISPINFO
notification
used in conjunction with the LPSTR_TEXTCALLBACK attribute. Here's the
code for the TVN_ITEMEXPANDING notification sent to WM_NOTIFY for processing:
The code for WM_NOTIFY - TVN_ITEMEXPANDING
case TVN_ITEMEXPANDING:
{ NMTREEVIEW * pnmtv;
HTREEITEM nmtvi;
pnmtv = (LPNMTREEVIEW)lParam;
TCHAR Text[256]={0};
tvi=((pnmtv)->itemNew);
nmtvi = TreeView_GetSelection(hTree);
if (NULL != nmtvi && tvi.hItem != nmtvi)
{ getNodeFullPath(hTree, tvi.hItem);
TreeView_SelectItem(hTree, tvi.hItem);
}
if ((pnmtv)->action & TVE_COLLAPSE)
{ tvi.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE;
tvi.iImage = 0; tvi.iSelectedImage = 1; TreeView_SetItem(hTree, &tvi);
return false;
}
tvi.mask = TVIF_IMAGE | TVIF_SELECTEDIMAGE;
tvi.iImage = 2;tvi.iSelectedImage = 3;TreeView_SetItem(hTree, &tvi); if (tvi.state & TVIS_EXPANDEDONCE)
return false; tvi.mask = TVIF_TEXT|TVIF_CHILDREN;
tvi.pszText=Text;
tvi.cchTextMax = 255;
if(TreeView_GetItem(hTree, &tvi))
{ if(tvi.pszText[1]==L':')
{ wsprintf(nodeCD, L"%s", Text);
SetCurrentDirectory(nodeCD);
nmtvi = TreeView_GetChild(hTree, tvi.hItem);
}
else
{ memset(&nodeCD,0,sizeof(nodeCD));
nmtvi = TreeView_GetChild(hTree, tvi.hItem);
} while (nmtvi)
{ memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.hItem=nmtvi;
tvi.mask = TVIF_TEXT|TVIF_CHILDREN;
tvi.pszText=Text;
tvi.cchTextMax = 255;
GetCurrentDirectory(MAX_PATH,nodeCD);
if(TreeView_GetItem(hTree, &tvi))
{ if(nodeCD[wcslen(nodeCD) - 1] != L'\\')
{ wsprintf(nodeCD, L"%s\\", nodeCD); // concat a back-slash to nodeCD
}
wsprintf(nodeCD, L"%s%s", nodeCD,tvi.pszText); // concat to nodeCD
getDirectories(tvi.hItem, nodeCD);
nmtvi = TreeView_GetNextSibling(hTree, tvi.hItem);
}
}
}
return false;
} break; // case TVN_ITEMEXPANDING
WM_NOTIFY - TVN_ITEMEXPANDED: Are we there yet?
The TVN_ITEMEXPAnDED notification is sent by the tree control after the node is expanded or collapsed.
If the action is TVE_COLLAPSE we exit.
Otherwise we look at the start in directory. If is is blank, that is, if the first character is zero we exit also. If the start in directory is there we get
the just expanded folder's children and concatenate their item text to the current directory and compare it to the start in directory. If one of the
children
is a sub-string of the start in directory and it has children, we use the TREEVIEW_EXPAND macro to expand the node. This will populate the treeview node's
grandchildren and continue the process in the TVN_ITEMEXPANDED notification that will be sent after expansion. If one of the children is the start
in directory we zap the start in directory with memset, setting it to zeroes, then select the child node, highlight it, and give it the focus. Let's take
a look at the code for the TVN_ITEMEXPANDED notification that is sent to WM_NOTIFY after the TVN_ITEMEXPANDING
notification.
The code for WM_NOTIFY - TVN_ITEMEXPANDED
case TVN_ITEMEXPANDED: { NMTREEVIEW * pnmtv;
HTREEITEM nmtvi;
pnmtv = (LPNMTREEVIEW)lParam;
TCHAR zero = L'\0';
TCHAR Text[256]={0};
TCHAR teXt[256]={0};
TCHAR text[256]={0};
if ((pnmtv)->action & TVE_COLLAPSE)
return 0; tvi=((pnmtv)->itemNew);
tvi.mask = TVIF_TEXT|TVIF_CHILDREN;
tvi.pszText=Text;
tvi.cchTextMax = 255;
if(TreeView_GetItem(hTree, &tvi))
{ if (*startInDir != zero)
{ memset(&nodeCD,0,sizeof(nodeCD));
nmtvi = TreeView_GetChild(hTree, tvi.hItem);
while (nmtvi)
{ tvi.hItem=nmtvi;
GetCurrentDirectory(MAX_PATH,nodeCD);
if(TreeView_GetItem(hTree, &tvi))
{ if(nodeCD[wcslen(nodeCD) - 1] != L'\\')
{ wsprintf(nodeCD,L"%s\\", nodeCD);
}
wsprintf(text,L"%s%s", nodeCD, tvi.pszText);
if (_wcsnicmp(startInDir, text, wcslen(text)) == 0 )
{
if ((_wcsicmp((startInDir), (text)))==0)
{
memset(startInDir,0,sizeof(startInDir));
TreeView_Select(hTree,nmtvi,TVGN_CARET);
TreeView_Select(hTree,nmtvi,TVGN_DROPHILITE);
SetFocus(hTree);
return false;
}
wsprintf(text,L"%s\\", text);
if (_wcsnicmp(startInDir, text, wcslen(text)) == 0 )
{
if (tvi.cChildren > 0 )
{
if (!(tvi.state & TVIS_EXPANDEDONCE ))
{ //node has children but has never been expanded. TVN_EXPANDED will take over now.
Sleep(300);
TreeView_Expand(hTree, tvi.hItem, TVE_EXPAND); // we are through here
return false; //terminate the routine
}
}
}
}
nmtvi = TreeView_GetNextSibling(hTree, tvi.hItem);
} // just in case the handle is corrupted
} // if you have the child, do the loop
} // is there a start-in-directory
} //you've got the requested attributes
return false;
} break; // case TVN_ITEMEXPANDED
WM_NOTIFY - NM_CLICK: Did you hit me again?
The NM_CLICK notification is sent by the tree control when the user left clicks the control. It is sent before the TVN_SELCHANGING
notification when
the node is not selected. This allows the program to control the selection process. You can
return zero to allow the selection or non-zero to disallow it.
But when the item is already selected, what happens? The default answer is -
nothing! The notification contains only an NMHDR structure. Its fields are
hwndFrom, idFrom, and code. The TreeView node is not identified. It seems to be pointless. Is this what we want?
No! This would be confusing. We expect the
response to be populating the ListView. The problem is, this is what is done in TVN_SELCHANGED with the TVC_BYMOUSE action code, but only when the selection
is changed BYMOUSE. We don't do it when the selection is changed BYKEYBOARD. We have a slight problem. All we know is the user has clicked on
something in the treeview. But what? The only information we are given is the
hwndFrom, idFrom, and code. The code is NM_CLICK. We need the cursor
position which we can get with a call to GetCursorPos. We adjust it relative to
hwndFrom with a call to the ScreenToClient function. We will put it into a
TVHITTESTINFO structure called lpht. Then we do a TreeView_HitTest with hwndFrom and
lpht. The return value from this macro is an HTREEITEM which we will
compare with the selected item in the treview. We could do this in a very simple statement like this
if ( TreeView_HitTest(hwndFrom, &lpht)!= (TreeView_GetSelection(hwndFrom))) return 0;
but in this instance I intentionally broke it apart to make sure it was obvious what was happening. Besides, it was easier to step through.
Now you may ask "Why do that?". The answer is, the default selection processing has not been done yet. We don't know where we are in the
File System. We could call the function that would give us that information but we would be duplicating code for no good reason. So we allow the selection
to occur. In truth we are duplicating some code because now we have to populate the listview the same way we would in the selection processing. We
could move that processing to a function and call it from both places. I just haven't done it yet. So it's like they say - it's six on one hand, and half a
dozen on the other. But the thing about having six on one hand is you can't flip the bird because you have no middle finger!
The code for WM_NOTIFY - NM_CLICK
case NM_CLICK:
{ HTREEITEM nmtvi;
NMHDR * lpnmh;
TCHAR Text[256]={0};
TCHAR text[256]={0};
lpnmh = (LPNMHDR) lParam;
UINT_PTR idFrom = (lpnmh)->idFrom;
UINT code = (lpnmh)->code;
HWND hwndFrom = (lpnmh)->hwndFrom;
TVHITTESTINFO lpht ={};
POINT pt;
GetCursorPos(&pt) ; ScreenToClient(hwndFrom,&pt); lpht.pt = pt ; TreeView_HitTest(hwndFrom, &lpht); HTREEITEM nmClickedtvi = lpht.hItem;
nmtvi = TreeView_GetSelection(hwndFrom);
if (nmtvi != nmClickedtvi) {return 0; } if (lpht.flags && TVHT_ONITEM ) { memset(&tvi,0,sizeof(tvi));
tvi.mask = TVIF_TEXT|TVIF_CHILDREN|TVIF_IMAGE ;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if(TreeView_GetItem(hwndFrom, &tvi))
{ GetCurrentDirectory(MAX_PATH,nodeCD);
if (tvi.iImage == 4)
{ LVITEM item;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iItem = 0;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = tvi.pszText;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret;
item.iSubItem = 1;
item.pszText = nodeCD;
ListView_SetItem(hList, &item);
int nRet = ret+1;
ListView_GetItemText(hList,nRet,0,text,255);
while (_wcsicmp(text, tvi.pszText)==0)
{
ListView_GetItemText(hList,nRet,1,text,255);
if (_wcsicmp(text, nodeCD)==0)
{
ListView_DeleteItem(hList,ret); ret = nRet;
}
nRet = nRet+1;
ListView_GetItemText(hList,nRet,0,text,255);
}
ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED); ListView_SetItemState(hList,-1,0,LVIS_SELECTED); ListView_SetItemState(hList,ret,LVIS_DROPHILITED,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_SELECTED,LVIS_SELECTED);
ListView_SetItemState(hList,ret,LVIS_FOCUSED,LVIS_FOCUSED);
ListView_EnsureVisible(hList,ret, true);
ReleaseCapture();
SetCapture(hList);
SetFocus(hList);
Sleep(1000);
ReleaseCapture();
} else if (tvi.cChildren == 1)
{ nmtvi=TreeView_GetChild(hwndFrom, nmtvi);
memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT | TVIF_CHILDREN| TVIF_IMAGE;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if(TreeView_GetItem(hwndFrom, &tvi))
{ ListView_DeleteAllItems(hList);
GetCurrentDirectory(MAX_PATH,nodeCD);
do { LVITEM item;
memset(&item,0,sizeof(item)); item.mask = LVIF_TEXT; item.iItem = 0; item.iSubItem = 0; item.cchTextMax = 260;
item.pszText = tvi.pszText;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret; item.iSubItem = 1; item.pszText = nodeCD;
ListView_SetItem(hList, &item); if (ret < 1) ret *= -1;
nmtvi = tvi.hItem;
nmtvi = TreeView_GetNextSibling(hwndFrom,nmtvi); memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
} while (TreeView_GetItem(hwndFrom, &tvi)); } } } } }break;
WM_NOTIFY - TVN_KEYDOWN: Get me outta this tree!
In the previous section, we saw that the user could click on an item and put that item or its children into the listview pane, but try as we might, we could not jump over to the listview pane by left clicking on the treview pane. If we wrote code to do that, it would be extremely counter intuitive. But what
if the user wants to jump over to the listview pane? In the next section, we have a function that provides the same results as a left click but does jump over to the listview pane, but it does it with the 'Enter' key. At first use, this does not 'tuit' the user's horn, so we need a way that will 'tuit', loud and clear. Well, you might say, 'Why not use the TAB
key?'. Okay, here's the code, short, sweet, and super simple.
The code for WM_NOTIFY - TVN_KEYDOWN
case TVN_KEYDOWN:
{
NMTVKEYDOWN * ptvkd;
ptvkd = (LPNMTVKEYDOWN) lParam;
if ((ptvkd)->wVKey==VK_TAB)
{
SetFocus(hList);
}
}break;
WM_NOTIFY - NM_RETURN: Hit me with your best shot!
It may seem at first glance that NM_RETURN gives us the same sort of problem as NM_CLICK because it also does not contain any information to identify
the node that sent the notification. But here there is no uncertainty about getting the proper
item. The simple fact is you can not change the selection of a TreeView node by pressing Enter or hitting Return if that is what is on your keyboard. Wherever the caret is in the tree, there it is! Pressing Enter
does not change it. All you have to do is get it. We can do this the same way we did for the NM_CLICK but we don't have to do a hit test. All we need is just
a TreeView_GetSelection macro to get the handle to the item that is selected. We put the
handle into a TVITEM member named hItem and call TreeView_GetItem.
Next we insert the item into the ListView unless it is a folder, in which case we clear the ListView, get the item's children, and put them in the ListView.
For a folder, this result should be what they expect. For a file, it may not be what they expect but since Windows Explorer does not put
files in the tree at
all, we are not confusing the user by inserting the file into the ListView, but only by placing the
file into the tree. Another difference is NM_RETURN can
transfer the keyboard focus to the ListView. With the same code, NM_CLICK can't do the same thing. You would have to move the cursor to the item's position
in the ListView. I haven't provided code to do that because the user has control of the mouse and I don't want to take it away from them. However
on the
keyboard I add a file to the ListView when the user presses Enter and sets focus on that
item in the ListView then move down one. I have provided
mechanisms for the user to move back to the TreeView without using the mouse. We will look at that next but now let us look at the
code for NM_RETURN.
The code for WM_NOTIFY - NM_RETURN
case NM_RETURN:
{ NMTREEVIEW * pnmtv;
HTREEITEM nmtvi;
TCHAR Text[256]={0};
TCHAR text[256]={0};
HWND result = NULL;
pnmtv = (LPNMTREEVIEW)lParam;
memset(&tvi,0,sizeof(tvi));
tvi.mask = TVIF_TEXT|TVIF_CHILDREN|TVIF_IMAGE ;
tvi.pszText=Text;
tvi.cchTextMax = 255;
nmtvi = TreeView_GetSelection(hTree);
tvi.hItem = nmtvi;
if(TreeView_GetItem(hTree, &tvi))
{
GetCurrentDirectory(MAX_PATH,nodeCD);
if (tvi.iImage == 4)
{ LVITEM item;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iItem = 0; item.iSubItem = 0; item.cchTextMax = 260;
item.pszText = tvi.pszText; int ret = ListView_InsertItem(hList, &item); item.iItem = ret; item.iSubItem = 1; item.pszText = nodeCD; ListView_SetItem(hList, &item); int nRet = ret+1;
ListView_GetItemText(hList,nRet,0,text,255);
while (_wcsicmp(text, tvi.pszText)==0)
{
ListView_GetItemText(hList,nRet,1,text,255);
if (_wcsicmp(text, nodeCD)==0)
{
ListView_DeleteItem(hList,ret); ret = nRet;
}
nRet = nRet+1;
ListView_GetItemText(hList,nRet,0,text,255);
}
ListView_SetItemState(hList,-1,0,LVIS_SELECTED); ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_DROPHILITED,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_SELECTED,LVIS_SELECTED);
ListView_SetItemState(hList,ret,LVIS_FOCUSED,LVIS_FOCUSED);
ListView_EnsureVisible(hList,ret, true);
ReleaseCapture();
SetCapture(hList);
SetFocus(hList);
ReleaseCapture();
}
else if (tvi.cChildren == 1)
{
nmtvi=TreeView_GetChild(hTree, nmtvi);
memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT | TVIF_CHILDREN| TVIF_IMAGE;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
if(TreeView_GetItem(hTree, &tvi))
{
ListView_DeleteAllItems(hList);
GetCurrentDirectory(MAX_PATH,nodeCD);
do
{
LVITEM item;
memset(&item,0,sizeof(item));
item.mask = LVIF_TEXT;
item.iItem = 0;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = tvi.pszText;
int ret = ListView_InsertItem(hList, &item);
item.iItem = ret;
item.iSubItem = 1;
item.pszText = nodeCD;
ListView_SetItem(hList, &item);
if (ret < 1) ret *= -1;
nmtvi = tvi.hItem;
nmtvi = TreeView_GetNextSibling(hTree,nmtvi);
memset(&tvi,0,sizeof(tvi));
memset(&Text,0,sizeof(Text));
tvi.mask = TVIF_TEXT;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi;
} while (TreeView_GetItem(hTree, &tvi));
}
}
} } break; } } break; } }break;
The other WM_SIZE message: Now that's a tight fit!
The WM_SIZE message is sent to a window after its size has changed. The window receives this message through its
WindowProc function, or, in this case,
its DialogProc function. In this instance the hTreeView
window was sized in the WndProc. For a window created with the
CreateWindow function with a
Window class of WC_TREEVIEW, that would be all that we have to do. But since this was created with the
CreateDialog function, we have to size the control
because it is also a window. If we don't size it, it will not fill the entire
hTreeView window and would not be fun. Try commenting out this WM_SIZE and take
a look at the results. Probably still workable, but, if you do the same to the WM_SIZE in the ListView procedure the results are not workable at all. This
is because the control is sized to fit the initial size of the window. The frame for the control is larger than the frame of a control in a window with a
Window class of WC_TREEVIEW or WC_LISTVIEW. We would like the control to fill the entire window, with no
dialog frame, since any button or whatever will be
in other windows. In the window procedure, WndProc, we extract the width and height from the
lParam and then we did a bunch of other stuff to split-up
the space between the three child windows. Doing this means the dialog window for the treeview has been sized and the window procedure for the
dialog receives a WM_SIZE message. Here we also extract the width and height from the
lParam. But here we don't need to do other stuff. Just move the
control, htree, to fill the window, hTreeView, completely. Then we return zero to signify that the message was handled. Notice the 'break' after the
closing brace, ending the code for the WM_SIZE message, followed by the right brace for the
switch block, followed by a 'return' statement. This
returns control from the dialog procedure back
to the message loop. Next we have the closing brace to end the treeview procedure. Since the code is exactly the same except for the HWND handle used, we
will not describe each individual WN_SIZE message, so you must refer to this section for an explanation of the code. Here is that code for WM_SIZE.
The code for the WM_SIZE message and the rest of the TreeView procedure
case WM_SIZE:
{
UINT width = LOWORD(lParam);
UINT height = HIWORD(lParam);
MoveWindow(hTree,0,0,width,height,true);
return 0;
}break;
}
return (INT_PTR)FALSE;
}
The Window Procedure for the ListView
In this Window Procedure we use the WM_INITDIALOG message to create two columns for 'Name' and 'Path' in the
listview control. In a 'CreateWindow' version
of a ListView this would be done in the WndProc WM_CREATE
message code as we did for the TreeView vontrol. But the purpose of putting the calls to the
initialization routines in the WM_CREATE code was for demonstration. That code should really be placed in the WM_INITDIALOG section for maximum re-usability.
We are using global variables for our window handles so the first thing we do is to get the handle for the control in this window. This window,
hDlg, is in
fact hListView and the control is IDC_LIST1, defined by me/you in the Dialog Editor or someone else, and imported by you/me. We set
hList to this handle. We
create an LVCOLUMN structure called lvCol and set its mask member to store
text, width, and subitem index values. The mask member is the set of flags
that tells the control what values to save when you are storing information and what values to give you back when you are retrieving them. Next we
set the cx member of lvCol to the initial width of the first
column, set the pszText member of lvCol to the name we have chosen for the
column header, and call the
ListView_InsertColumn function with the handle to
the ListView, hList, the column index, zero, and a reference to lvCol. We repeat this, re-using lvCol, changing the text member for the next column, the
cx
member for the width of the column, and passing one for the column index of the second column. This will create two columns with Column Headers. To add any
additional columns just repeat this procedure, increasing the column index for each one. Let's look at
the code for WM_INITDIALOG.
INT_PTR CALLBACK listViewProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
{
hList = GetDlgItem(hDlg,IDC_LIST1);
LVCOLUMN lvCol;
memset(&lvCol,0,sizeof(lvCol));
lvCol.mask=LVCF_TEXT|LVCF_WIDTH|LVCF_SUBITEM; lvCol.pszText=L"Name";
lvCol.cx=0x96; ListView_InsertColumn(hList,0,&lvCol);
lvCol.pszText=L"Path";
lvCol.cx=0x0196; ListView_InsertColumn(hList,1,&lvCol);
}
return (INT_PTR)TRUE;
The ListView's WM_NOTIFY
The code for the switch block in this WM_NOTIFY block could be used in a
CreateWindow version of a listview by changing only the 'case WM_NOTIFY' to
'case IDC_LIST1' This may seem non-sensical since IDC_LIST1 is a control that should be sending event notifications to a window and WM_NOTIFY should be
processing the notifications. The key is this; in a CreateWindow the WM_NOTIFY handles the notifications from every common control in the window, so we are
assuming this is being inserted into a WM_NOTIFY in the CreateWindow version. Also, we don't need to identify the control in this procedure because there
is only one control, IDC_LIST1. In the CreateWindow version, we have to identify the control sending the notification so we end up changing WN_NOTIFY to
IDC_LIST1. To clarify the changes we would make, we are deleting the WM_NOTIFY and inserting IDC_LIST1 and inserting the whole thing into a WM_NOTIFY.
Now, let's discuss the notifications processed by WM_NOTIFY, beginning with NM_DBLCLK - the left button of the mouse has been double clicked. There
are two structures that can be used to map the value in the lParam, NMITEMACTIVATE and NMLISTVIEW. The important difference is the uKeyFlags member of the
NMITEMACTIVATE structure identifies the Modifier Key that was pressed when the button was double clicked. We don't use it here so we will use NMLISTVIEW
because we are using it for the other user actions. The remainder of the code is the same in either case. We allocate our variable storage, including a
LVITEM, which we call 'lvi'. We set the iItem member of lvi to the value of the member with the same name in the NMLISTVIEW structure, that is,
lvi.iItem = (pnmlv)->iItem;
Then we compare it to zero to determine whether to continue processing. If we are continuing we set the mask to
LVIF_TEXT. The pszText member
of lvi is a pointer to a buffer and we point it at the 'Text' buffer allocated earlier. The
iSubItem is zero from the memset function call. We use the
ListView_GetItem macro to get the text from the first column of the row indexed by
iItem. Now we set the iSubItem member to one, the pszText pointer to point
at the 'text' buffer, with the lower case 't', and get the item text. We copy both of them into the 'teXt' buffer, with an upper case 'X', separating
them with a backslash. This is the full path with filename of the ListView
item that was double clicked. We call the '_wstat' function with the 'teXt' buffer
and a reference to a '_stat' struct named buf. There is a function with the name '_stat' and a struct named '_wstat' but the only combination I could get to
work was the '_stst' struct and the '_wstat' function call. The result of this function call is zero if it is successful, in which case we AND the BITS of
the st_mode member of buf with the _stat structure constant _S_IFREG to determine if it is a regular file, and if it is we put quotes around the full pathname
and 'open' it with a call to the ShellExecute function, specifying SW_SHOWNORMAL for the SetShowWindow command. This will open a file using the defined file
association. If it is _S_IFDIR we will use 'explore'. The effect is we are opening a
directory in Windows Explorer and a file with its associated program.
Here is the code for the ListView WM_NOTIFY and the first notification processed by the ListView WM_NOTIFY
message handler, NM_DBLCLK.
The ListView WM_NOTIFY's NM_DBLCLK:
case WM_NOTIFY:
{
switch (((LPNMHDR)lParam)->code){
case NM_DBLCLK:{ NMLISTVIEW * pnmlv;
LVITEM lvi;
int result;
struct _stat buf;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
pnmlv =(LPNMLISTVIEW)lParam;
memset(&lvi,0,sizeof(lvi));
lvi.iItem = (pnmlv)->iItem;
if (lvi.iItem == -1) return 0;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(teXt,L"%s\\%s", text,Text);
result = _wstat(teXt, &buf );
if (result == 0)
{
if((buf.st_mode & _S_IFREG))
{
wsprintf(teXt,L"\"%s\\%s\"", text,Text);
SetWindowText(hEdit,teXt);
ShellExecute(NULL, L"open", teXt, NULL, NULL, SW_SHOWNORMAL);
}
if((buf.st_mode & _S_IFDIR))
{
wsprintf(teXt,L"\"%s\\%s\"", text,Text);
SetWindowText(hEdit,teXt);
ShellExecute(NULL, L"explore", teXt, NULL, NULL, SW_SHOWNORMAL);
}
}
}break;
The ListView WM_NOTIFY's LVN_KEYDOWN - Delete, Down, Up, Tab, and F3
The LVN_KEYDOWN notification processes five keys, the delete key, the down
arrow key, the tab key, the up arrow key, and the F3 key. If the key pressed was
none of these it returns zero. F3 is last because it involves the most processing and takes more time to scroll through, so it's last.
For the DELETE key, we check to see if the handle to hdelBar is NULL,
and call the CreateDialog function if so to create it. We get the Window Rectangle of
hWnd to determine the position of the dialog box and Client Rectangle of
hWnd to determine its width. For its height we use an arbitrary number, fifty,
because it looks okay. Or maybe it's an imaginary number because we dreamed it up. If the handle is not
NULL, it has been created, so we show it.
The UP arrow key and the DOWN arrow key provide the expected information, via the Edit Control, when the keys are pressed. Since multiple items can be
selected and highlighted it is also necessary to re-set the state of any item that
is selected or
highlighted. Using minus one for the index will cause this to
occur for all items. This is the simplest way to do it, so we call ListView_SetItemState with the handle to
hList as the first parameter, minus one as the
item index, zero as the state parameter, and in separate calls, the mask is LVIS_DROPHILITED and in the second call, LVIS_SELECTED. This clears the bits
for everything. We do this for both the UP and DOWN keys. But since we need to get the Path and Filename of the item that will be
highlighted by the control
after we exit, we need to know which item we are moving from so that we can get the previous one for the UP Key or the next one for the Down Key. We also
need to know when we hit a boundary so that we can do nothing but exit. For the Down Key we call the macro ListView_GetItemCount to get the number of items
in the list. This will give us the boundary at the BOTTOM (the upper bound). Then we get the item we are moving from by calling ListView_GetNextItem with the
handle to hList, minus one for the index to start from and LVNI_SELECTED to get the selected item. The control will select the next item unless it is at
the bottom already. We test for a result less than itemCount minus one to determine whether to get the full
path of the next one or just get out if it
isn't. Now we get the text for the Path and Filename and copy them into a buffer, placing a backslash in between them and put them into the Edit Control.
Having fetched the selected item and gotten the information for the Edit Control, we re-set the state for everything and return zero. Now the
control does its processing and we see the last item selected, highlighted, and focused. It's all good and in-tuit-ive. As for the UP arrow key, we only
need to make sure the result is greater than zero so we can subtract one from it. If it is less than one we return zero and exit. We do not even care how
many items there are. There is one issue using the path in the Edit Control if you use the
ISEARCH feature of the ListView. I haven't written code to
refresh the information after the selection is moved so the intuitiveness is lost. For those who may rely upon this information, I will write that code when I
get a round tuit but they are extremely hard to find.
If the KEY is a TAB we just set the focus on the treeview. We want to keep it short, sweet, and super simple. I'll let you figure out the rest of it.
The processing of the F3 key is the complicated part of the LVN_KEYDOWN
notification. We are using the F3 key to 'FIND' the file that has the selected
state in the ListView, and locate it in the tree. We have received no information in
lParam to identify the item that has the selected state. So we call the
ListView_GetNextItem macro, with three parameters, hList, minus one(-1), and LVNI_SELECTED. This tells the macro to look in
hList to find the item after
the minus one (zero) item that is selected. In other words, to start at the
beginning. We store the result in result. That's simple. If the result is minus one
we get out because nothing is selected. Otherwise we set the LVITEM index,
iItem to result, iSubItem to zero, mask to LVIF_TEXT, cchTextMax to the length of
the Text buffer previously allocated minus one(255), and the pazText pointer to
Text. We call the macro ListView_GetItem to give us the filename from the
first column. Then we change the iSubItem (the column index) to one and the
pszText pointer to point to text. We call the ListView_GetItem macro again to
get the path of the filename. The next thing to do is copy text, a backslash, and
Text into startInDir, using
wsprintf to do all of the work in one step.
Next we store the wcslen(gth) of the startInDir in
sidLen, for later use. Now we get theSelectedItem by calling the TreeView_GetSelection macro with the
single parameter, hTree. This will get the item that is currently selected in the
tree. Now we drop down to the ROOT of the tree and begin the climb back up
to locate our file by calling the TreeView_GetRoot macro and storing the result in
nmtvi. This gives us the handle to the first item in the Tree.
Now we begin to loop while nmtvi is not zero. If we had failed to get the Root the loop will never be executed. Now in the loop, we prepare our
TVITEM tvi
and set the hItem member of tvi to nmtvi, then call the TreeView_GetItem macro to get the attributes we requested in the mask member and if successful, we
compare the buffer pointed at by the pszText member and startInDir,
ignoring case but comparing only for the length of the text in the buffer, not including
the zero that ends the text. Not matching, we get the next sibling and repeat the loop until there are no more siblings. Matching the
startInDir, we check
to see if the item has not been expanded, calling the TreeView_Expand macro to request a TVE_EXPAND action, expanding the folder item.
When we find an expanded item we compare nmtvi to theSelectedItem. When they match, we call the macro TreeView_GetNextItem to change theSelectedItem
to TVGN_NEXT, the one after nmtvi. Now we make it the selected item by calling
TreeView_SelectItem to make the new theSelectedItem the selected item. This will
cause a TVN_SELCHANDING and TVN_SELCHANGED, but that processing is only a short, one time delay. Whether they match or not, the next statement will select
nmtvi, getting us back on the process of locating the requested file in the
tree.
The code for the ListView WM_NOTIFY's LVN_KEYDOWN notification:
case LVN_KEYDOWN: {
NMLVKEYDOWN * pnkd;
pnkd = (LPNMLVKEYDOWN) lParam;
HTREEITEM nmtvi;
RECT lrccl, crccl, drccl;
if ((pnkd)->wVKey == VK_DELETE)
if (hdelBar != NULL)
{
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hdelBar,&drccl);
MoveWindow(hdelBar,lrccl.left+8,lrccl.top+24,crccl.right,50,true);
ShowWindow(hdelBar, SW_SHOW);
}
else
{
hdelBar = CreateDialog(GetModuleHandle(NULL),MAKEINTRESOURCE(IDD_DIALOGBAR1),hDlg,delBarProc);
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hdelBar,&drccl);
MoveWindow(hdelBar,lrccl.left+8,lrccl.top+24,crccl.right,50,true);
ShowWindow(hdelBar, SW_SHOW);
}
if ((pnkd)->wVKey == VK_DOWN)
{
LVITEM lvi;
int result;
int itemCount;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
memset(&lvi,0,sizeof(lvi));
itemCount =ListView_GetItemCount(hList);
result = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
if (result < itemCount -1)
lvi.iItem = result+1;
else
return 0;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(teXt,L"%s\\%s", text,Text);
SetWindowText(hEdit,teXt);
ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED); ListView_SetItemState(hList,-1,0,LVIS_SELECTED); return 0;
}
if ((pnkd)->wVKey == VK_TAB)
{
SetFocus(hTree);
return 0;
}
if ((pnkd)->wVKey == VK_UP)
{
LVITEM lvi;
int result;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
memset(&lvi,0,sizeof(lvi));
result = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
if (result < 1) return 0;
lvi.iItem = result-1;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(teXt,L"%s\\%s", text,Text);
SetWindowText(hEdit,teXt);
ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED); ListView_SetItemState(hList,-1,0,LVIS_SELECTED); return 0;
}
if ((pnkd)->wVKey != VK_F3)
{
return 0;
}
LVITEM lvi;
int result;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
memset(&lvi,0,sizeof(lvi));
result = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
if (result == -1)
{
return 0; }
lvi.iItem = result;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(startInDir,L"%s\\%s", text,Text); sidLen = wcslen(startInDir);
memset(nodeCD,255,sizeof(nodeCD));
HTREEITEM theSelectedItem = TreeView_GetSelection(hTree);
nmtvi = TreeView_GetRoot(hTree);
while (_wcsnicmp(startInDir, nodeCD, wcslen(nodeCD))!=0)
{
memset(&tvi,0,sizeof(tvi));
tvi.mask = TVIF_TEXT|TVIF_CHILDREN|TVIF_IMAGE ;
tvi.pszText=Text;
tvi.cchTextMax = 255;
tvi.hItem = nmtvi; if(TreeView_GetItem(hTree, &tvi))
{ if (_wcsnicmp(startInDir, tvi.pszText, wcslen(tvi.pszText))==0)
{
if (tvi.cChildren > 0 )
{
if (tvi.state == 0 )
{ TreeView_SelectItem(hTree,tvi.hItem);
Sleep(100);
TreeView_Expand(hTree, tvi.hItem, TVE_EXPAND); return 0; }
else if (tvi.state & TVIS_EXPANDEDONCE)
{ SetCurrentDirectory(tvi.pszText);
if (theSelectedItem == nmtvi)
{ theSelectedItem = TreeView_GetNextItem(hTree,nmtvi,TVGN_NEXT);
TreeView_SelectItem(hTree,theSelectedItem);
}
TreeView_SelectItem(hTree,nmtvi);
return 0;
}
}
}
else
{
nmtvi = TreeView_GetNextSibling(hTree,nmtvi); }
}
}
} break;
The ListView WM_NOTIFY's NM_RETURN notification
The ListView sends an NM_RETURN when it has focus and the user presses Enter. This notification identifies the window, the control, and the action
code, in the NMHDR structure that the lParam points to. In this
dialog we know all of these things but we need to identify the item that is selected in the
control, hList. We use the ListView_GetNextItem macro with three parameters,
hList, minus one, and the constant
LVNI_SELECTED. This tells the macro to
get the next selected item, starting after the minus one item, that is the zero item - the beginning of the list. We call it
result and if it is minus one,
then nothing was selected and we get out. Result is the index to the Rows in the ListView. In an LVITEM we call it
iItem. We initialize an LVITEM, calling
it lvi, we put result in the iItem member of lvi. We call the index for the columns
iSubItem. It is a "one-based index of the subitem to which this
structure refers, or zero if this structure refers to an item rather than a subitem" according to the Microsoft Help files. From my perspective this
means it is really a zero-based index, just like the iItem index. And it is already set to zero because we used
memset to fill lvi with zeroes. We use the
ListView_GetItem macro to get the pszText from the first two columns, changing the
iSubItem index to one for the second call and put them into the
Text
and text buffers. We copy text, a backslash, and Text into the
teXt buffer using a call to wsprintf to do it in a single step. We re-use
result to store the
result of a call to _wstat to get the file-status information about the path in teXt, storing the information in a buffer mapped by the
_stat struct buf. The result is used to return error information. It will be zero if the information is obtained. For this notification we only need to know if it is a
file or directory. We use ShellExecute to 'open' a file and to 'explore' a directory. A double click of the mouse will produce the same result but the item
selected is provided by the NMLISTVIEW structure or by the NMITEMACTIVATE structure that is pointed to by the NM_DBLCLK
notification's lParam instead of
the NMHDR structure that the NM_RETURN notification's
lParam points to here.
The code for the ListView WM_NOTIFY's NM_RETURN notification:
case NM_RETURN: {NMLISTVIEW * pnmlv;
LVITEM lvi;
int result;
struct _stat buf;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
pnmlv =(LPNMLISTVIEW)lParam;
memset(&lvi,0,sizeof(lvi));
result = ListView_GetNextItem(hList,-1,LVNI_SELECTED);
if (result == -1) return 0;
lvi.iItem = result;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(teXt,L"%s\\%s", text,Text);
result = _wstat(teXt, &buf );
if (result == 0)
{
if((buf.st_mode & _S_IFREG))
{
wsprintf(teXt,L"\"%s\\%s\"", text,Text);
SetWindowText(hEdit,teXt);
ShellExecute(NULL, L"open", teXt, NULL, NULL, SW_SHOWNORMAL);
}
if((buf.st_mode & _S_IFDIR))
{
wsprintf(teXt,L"\"%s\\%s\"", text,Text);
SetWindowText(hEdit,teXt);
ShellExecute(NULL, L"explore", teXt, NULL, NULL, SW_SHOWNORMAL);
}
}
}break;
The ListView WM_NOTIFY's NM_CLICK notification
An NM_CLICK notification is sent by the ListView control when a listview item is clicked by the user. The
lParam parameter is a pointer to a NMITEMACTIVATE
structure with information provided by the control. This structure is identical to NMLISTVIEW but it has a
uint field added that identifies the Modifier Key
that was depressed when the user clicked. We don't care about this field so we are using NMLISTVIEW. The important work we do here is the macros to set
the item state. There are five of them in a row. Why do that? Is it really
necessary? My answer is, I'm not sure. The first two clear the selected bit and the
drophilited bit for everything. That's the minus one parameter that tells the macro to do everything. The next two set the bits for the item that was clicked.
I tried to 'OR' the bits but it did not work for me. The documentation suggested that it should, so
I'm not sure what the problem is. So I did them
individually for now. Some day, I may achieve understanding and or enlightenment. That is why I explore, after all.
The rest of the code just gets the full path of the item and puts it in the Edit Box. It's pretty much the same as every where else.
The code for the ListView WM_NOTIFY's NM_CLICK notification
case NM_CLICK: {NMLISTVIEW * pnmlv;
LVITEM lvi;
TCHAR Text[256]={0};
TCHAR text[256]={0};
TCHAR teXt[256]={0};
pnmlv =(LPNMLISTVIEW)lParam;
memset(&lvi,0,sizeof(lvi));
ret = (pnmlv)->iItem;
lvi.iItem = (pnmlv)->iItem;
lvi.mask = LVIF_TEXT;
lvi.cchTextMax =255;
lvi.pszText = Text;
ListView_GetItem(hList,&lvi);
lvi.pszText = text;
lvi.iSubItem = 1;
ListView_GetItem(hList,&lvi);
wsprintf(teXt,L"\"%s\\%s\"", text,Text);
SetWindowText(hEdit,teXt);
ListView_SetItemState(hList,-1,0,LVIS_DROPHILITED); ListView_SetItemState(hList,-1,0,LVIS_SELECTED); ListView_SetItemState(hList,ret,LVIS_DROPHILITED,LVIS_DROPHILITED);
ListView_SetItemState(hList,ret,LVIS_SELECTED,LVIS_SELECTED);
ListView_SetItemState(hList,ret,LVIS_FOCUSED,LVIS_FOCUSED);
}break;
The ListView WM_NOTIFY's NM_RCLICK notification
NM_RCLICK provides the same functionality as the DELETE Key (for a description of that processing, see the section on LVN_KEYDOWN(VK_DELETE)). It is really
just a place holder for a short cut menu that will include this function. For now it is just another way to get to the listview clean-up dialog.
The code for the ListView WM_NOTIFY's NM_RCLICK notification:
case NM_RCLICK:{ NMLISTVIEW * pnmlv;
LVITEM item;
pnmlv =(LPNMLISTVIEW)lParam;
TCHAR Text[256]={0};
memset(&item,0,sizeof(item));
item.iItem = (pnmlv)->iItem;
item.mask = LVIF_TEXT;
item.iSubItem = 0;
item.cchTextMax = 260;
item.pszText = Text;
int ret = ListView_GetItem(hList,&item);
RECT lrccl, crccl, drccl;
if (hdelBar != NULL)
{
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hdelBar,&drccl);
MoveWindow(hdelBar,lrccl.left+8,lrccl.top+24,crccl.right,50,true);
ShowWindow(hdelBar, SW_SHOW);
}
else
{
hdelBar = CreateDialog(GetModuleHandle(NULL),MAKEINTRESOURCE(IDD_DIALOGBAR1),hWnd,delBarProc);
GetWindowRect(hWnd,&lrccl);
GetClientRect(hWnd,&crccl);
GetClientRect(hdelBar,&drccl);
MoveWindow(hdelBar,lrccl.left+8,lrccl.top+24,crccl.right,50,true);
ShowWindow(hdelBar, SW_SHOW);
}
} break; } }break;
The ListView's WM_SIZE message
Looking back to the TreeView's WM_SIZE message, change the HWND hTree to
hList and the code would be identical.
The code for the ListView's WM_SIZE message
case WM_SIZE:
{
UINT width = LOWORD(lParam);
UINT height = HIWORD(lParam);
MoveWindow(hList,0,0,width,height,true);
return 0;
}break;
}
return (INT_PTR)FALSE;
}
The SetTreeviewImagelist function
The FONT for the TreeView Dialog was set in the IDD_FORMVIEW1 template using the Dialog Editor. To make this look the way it should, we need a larger
set of bitmaps. I created a set of bitmaps that have different images for the selected and non-selected states of the treeview items. The shapes are the same
but the colors are different, lighter for selected items. The ImageList_Create function has five parameters. The first two are the width and height of
each image. This also controls the size of the plus and minus buttons of the folders. I used thirty two for each of them. If you change the
font, you might
need to change these values also. The third controls the numver of bits for the color. The fourth is the starting number of images and the fifth is the number
of images by which the image list can grow. We save the handle to the HIMAGELIST in a storage location we named
hImageList. The next function we call is the
LoadBitmap function to load the IDB_BITMAP1 bitmap we created in the Resource Editor and add the bitmap to the ImageList, passing
NULL for the bitmap mask.
We delete the bitmap object and add the ImageList to the treeview. In the Window Procedure,
WndProc, we call the ImageList_Destroy function to clean-up
because the tree control doesn't destroy or delete it. You could put it in the treeview dialog proc but you would first need to add a WM_DESTROY
message
handler to that procedure. Six on one hand, ... Here is the code for the SetTreeviewImagelist
function.
The code for the SetTreeviewImagelist function:
bool inline SetTreeviewImagelist(const HWND hTv)
{
hImageList=ImageList_Create(32,32,ILC_COLOR32,6,1);
hBitMap=LoadBitmap(hInst,MAKEINTRESOURCE(IDB_BITMAP1));
ImageList_Add(hImageList,hBitMap,NULL);
DeleteObject(hBitMap);
TreeView_SetImageList(hTree,hImageList,TVSIL_NORMAL); return true;
}
The InitTreeViewItems function:
This function populates the treeview with any logical drive mounted on the file system, i.e., the root directory of the drive and one level of sub
directories, by calling the AddItemToTree function and the getDirectories function for each drive root. It also begins the process to find the
startInDir.
Here's how it works. The LogicalDriveStrings that we Get have an alpha character followed by a colon, a backslash, and a zero string terminator for each drive.
We define a string with space for an alpha character, the colon, backslash, and
a zero string terminator. This is pointed to by the pointer 'd'. The Drive
Strings is pointed to by the pointer 'p'. We copy the alpha character to our string, using the syntax '*d=*p'. This string is passed to the AddItemToTree
function as szTemp, to insert the Root folder into the tree and store the handle to that item in the global variable, nodeParent. Next we call the function
getDirectories with nodeParent and szTemp. It calls AddItemToTree, populating the children of the Root, but no more. In subsequent processing, we will always
populate the children of a folder before we expand its parent. This makes the tree seem to be fully populated but is in reality sparsely populated.
When we find the character that is the same as the first character of the array of characters that is pointed to by startInDir, we expand the node we
have just added to the treeview control. Expansion of a treeview node causes a TVN_ITEMEXPANDED
notification to be generated and processing that results in another
TVN_ITEMEXPANDING notification. When we locate the startInDir it is selected and the process ends. This is the process to
locate the file in the tree. Here it finds the start in directory.
Next we advance the pointer 'p' with a post increment operator while it points at something that is not zero. Since it is post increment, it will be
pointing at the position after the zero terminator. The string returned by GetLogicalDriveStrings has two zeroes at the end. That means the pointer 'p' is
pointing at the second zero after the last string in the LogicalDriveStrings has been processed. Then the
while(*p) loop terminates because 'p' is pointing at
zero. This loop occurs in the 'true' block of an if statement. The 'false' block or
else block consists of a 'return false' statement.
This process will leave the startInDir Hilited but not necessarily visible. To make sure it is visible we get the selected item in the tree and we
select again. This will select the item and scroll it into view or re-draw it with the TVGN_DROPHILITE style.
The code for the InitTreeViewItems function:
BOOL InitTreeViewItems(HWND hwndTV)
{
HTREEITEM hti; TCHAR szDTemp[] = TEXT(" :\\"); // first position will cantain drive letter.
/* Define string to hold Drive List.*/ TCHAR szTemp[BUFSIZE];
/* add a root item to the tree */
hti = (HTREEITEM)TVI_ROOT; //initialize root item
if (GetLogicalDriveStrings(BUFSIZE-1, szTemp))
{ /* szTemp comtains list of drives */
TCHAR* p = szTemp; /* point p at 'A' in 'A:\\\0C:\\\0...\0\0' */
TCHAR* d = szDTemp; /* point d at blank space in ' :\\\0' */
do
{ /* loop through string of drives */
*d = *p; /* copy 1 tchar from p to d */
nodeParent = AddItemToTree(hwndTV, hti, szDTemp); /* add a root item to the tree at level 2 */
getDirectories(nodeParent, szDTemp); /* add it's subdirectories at level 2 */
if (hti == NULL) /* you couldn't add the item */
return FALSE; /* go back and tell them it failed */
if (NULL == nodeParent)
{
return FALSE; /* go back and tell them it failed */
}
else if (*d == *startInDir)
{
TreeView_Expand(hTree, nodeParent, TVE_EXPAND);
}
while (*p++); // loop until p points at a '\0', incrementing p afterwaeds.
}
while (*p); /* end of string when p points at second '\0' of pair.*/
}
else
return FALSE; /* go back and tell them you couldn't get drive string.*/
nodeParent = TreeView_GetSelection(hTree);
TreeView_Select(hTree,nodeParent,TVGN_FIRSTVISIBLE);
return TRUE; /* go back and tell rhem it's okay. */
}
The getDirectories function:
This function uses the File Management functions FinFirstFile, FindNextFile, FindClose and the WIN32_FIND_DATA
structure. We pass this function the handle to a treeview item which we named
hDir
and the directory we want to get the subdirectories of, to populate our treeview item's children with. To begin the search we must supply the path and the
file to search for. This means the part after the last backslash is the name we want to find. But, we want to find everything in this directory. We can
use wildcards and the one that will match everything is an asterisk. We look at the character before the last one to see if it is a backslash. If it is we
add an asterisk to the path, otherwise we add a back slash and an asterisk, then call FindFirstFile with this path and the WIN32_FIND_DATA
structure. If the
call returns a search handle we do a loop to get all of the found files or directories. We pass the
image and SelectedImage indexes along with the handles to
the control and the treeview item, and the cFileName to the AddItemToTree function. When there is nothing else found we pass the search handle to
FindClose to
close the search handle. We have dropped the self references (dot and dotdot) and did not do any error handling. Any errors will result in the folder not
showing all of its children. We do not populate the listview here or store any of the file attributes, but they are available if you want to use them.
The code for the getDirectories function:
void getDirectories(HTREEITEM hDir, LPTSTR lpszItem) {
HANDLE hFind;
WIN32_FIND_DATA win32fd;
TCHAR szSearchPath[_MAX_PATH];
LPTSTR szPath=lpszItem;
DWORD dwError=0;
if(szPath[wcslen(szPath) - 1] != L'\\')
{
wsprintf(szSearchPath, L"%s\\*", szPath); }
else
{
wsprintf(szSearchPath, L"%s*", szPath); }
if((hFind = FindFirstFile(szSearchPath, &win32fd)) == INVALID_HANDLE_VALUE) return;
do {
if(win32fd.cFileName[0] != L'.'){
if ((win32fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
tviiImage = 0; tviiSelectedImage = 1; }
else
{
tviiImage = 4; tviiSelectedImage = 5; }
AddItemToTree(hTree, hDir, win32fd.cFileName);
}
} while(FindNextFile(hFind, &win32fd) != 0);
FindClose(hFind);
}
The AddItemToTree function:
The AddItemToTree function inserts an item into the tree as a child of hDir, which itself starts out as TVI_ROOT. It is inserted after
hPrev, the previously
inserted item. This function is really just setting up the TVINSERTSTRUCT with the information supplied and calling the TreeView_InsertItem
macro with that
structure. It stores the handle to the item returned in the static HTREEITEM variable hPrev and then also returns it to its calling program. It seems to be a
good candidate for inlining. Notice that the lParam field of the TVITEM structure is not being used. If you want to use it, go right ahead.
The code for the AddItemToTree function:
HTREEITEM inline AddItemToTree(HWND hwndTV, HTREEITEM hDir, LPTSTR lpszItem){
TVITEM tvi;
TVINSERTSTRUCT tvins;
static HTREEITEM hPrev = (HTREEITEM)TVI_FIRST;
tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
tvi.pszText = lpszItem; tvi.cchTextMax = NULL; tvi.iImage = tviiImage;
tvi.iSelectedImage = tviiSelectedImage;
tvins.item = tvi;
tvins.hInsertAfter = hPrev;
tvins.hParent = hDir; hPrev = TreeView_InsertItem(hwndTV, &tvins); return hPrev;
}
The getNodeFullPath function:
The getNodeFullPath function recursively gets the current treeview item's parent until it finds the drive, then it returns the tvi.pszText and collects
it to build the full path of the current treenode by using wsprintf to concatenate all of the item texts together in reverse order, in effect using the
program stack as a FIFO stack. This routine is called when a node is selected or when a button (plus or minus) is clicked. This is necessary because
clicking a button does not select a node, it only expands it or collapses it. When the tree has been fully populated this works perfectly, leaving
the selected item untouched. But with the method we are using, that is, minimal population, expanding a tree node does not populate its childrens' children.
Therefore the tree would break down and fall. We have to populate the node ourselves. When a request to expand a node is received, via a TVN_ITEMEXPANDING
notification, the code determines if it is selected. If not then we must programmatically select the node to get its full path in order for the tree to
function as the user expects. The TREENODE must accurately reflect the
contents of the file system, otherwise getDirectories will not find
files or directories because the PATH we pass to the function will not exist. Since the path doesn't exist, the node will have no children and no buttons.
The process to get the full path for the node is deceptively simple. It receives the handle to an item and the control it is in.
It gets the item text.
If the second character is a colon, it sets the text in the edit control and sets the current directory. Then it returns true, ending the recursion. Note
that this
is the first thing we do. We have found the root of the path but we don't know where the other end is. It could have any number of path parts. If this is
the last part we will return to the routine that called getNodeFullPath, otherwise we will return to the same routine, getNodeFullPath, to collect the item
text. When the second character is not a colon it gets the parent item's handle and makes the recursive call. Notice that
tvi, the TVITEM is allocated on
the stack. Also the buffer for tvi.pszText. We are essentially pushing the data on to the stack. How's your memory, got enough? It will loop until it finds
the colon or fails to get the item attributes that were requested by tvi.mask, then it will return in both cases. The
Text buffer pointed to by tvi.pszText
should contain the second item's text, such as "Users", when it has returned for the first time, and
nodeCD should contain the root of the path, such
as, "C:\". We look at the next to the last character (the last character is zero) to determine whether or not to put a backslash between nodeCD and
Text as we copy them into nodeCD using wsprintf. Then we set the current directory and the text in the edit control and return. Each time it comes back,
it pops the stack and adds the pszText to the end of nodeCD. It doesn't know or care when it is finished. It just goes back to work or whatever.
The code for the getNodeFullPath function:
BOOL getNodeFullPath(HWND hTree, HTREEITEM nmtvi){
TVITEM tvi;
TCHAR Text[256]={};
tvi.hItem=nmtvi;
tvi.mask = TVIF_TEXT;
tvi.cchTextMax = 255;
tvi.pszText=Text;
if(TreeView_GetItem(hTree, &tvi))
{
if (tvi.pszText[1]== L':')
{ wsprintf(nodeCD, L"%s", tvi.pszText); SetCurrentDirectory(nodeCD);
SetWindowText(hEdit, nodeCD); return true; }
else
{
nmtvi = TreeView_GetParent(hTree, tvi.hItem);
getNodeFullPath(hTree, nmtvi);
if (tvi.pszText)
{
if(nodeCD[wcslen(nodeCD) - 1] != L'\\')
{
wsprintf(nodeCD, L"%s\\%s", nodeCD,tvi.pszText); }
else
{
wsprintf(nodeCD, L"%s%s", nodeCD,tvi.pszText); }
SetCurrentDirectory(nodeCD);
SetWindowText(hEdit, nodeCD);
}
}
}
return true; }
The About dialog proc
This Dialog Procedure was included in the project by the project creation wizard. I am including it in this article so that readers can see the basic dialog
proc and compare it with a dialog proc for a common control. Specifically, note that a "Button" is handled in a
WM_COMMAND block (i.e., ID_OK - 'OK'
button), while a TreeView control is handled in a WM_NOTIFY block (i.e.,
IDC_TREE1 - the treeview control).
The code for the About dialog proc:
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
Epilog
An afterword on the subject of going native; I began this article with an allusion to transportation, claiming that I did not wish to be a tourist in my
exploration of 'modern' computer programming. This was not meant to denigrate the work of those who may write code in VB or C# with a very tight time
line for completing one phase before beginning another. I don't have a deadline, so I write code for fun. Sometimes in F# and sometimes in C/C++.
I view high level languages such as VB, C#, F#, or whatever as a vehicle to carry a lot of work to completion very quickly, not necessarily caring about
speed but more concerned with safety and accuracy, while C/C++ is the inner workings of that vehicle, i.e., the engine or transmission.
That high level vehicle has parts built-in that allow you to do powerful things, but the power is provided by those who wrote the language and they
did not provide the ability to do everything. In some cases, it is necessary to add a new part, some function in a different language, and link it in.
C/C++ is likely to be that different language in which you write that new part. In all likelihood you could write the entire application in C but that would
require some multiple number of lines of code to get the same functionality as in the higher level language. C++ has many features that can reduce the
additional coding needed to get that functionality in your program. In particular, the frameworks available are in some cases even more powerful than
some high level languages. Chances are you will always find something that a particular
framework does not do for you. You can find another framework
that does what you want or if you work hard enough you can do it yourself. As in sports, the more you train, the stronger you get! The more you rely on any
shortcut, whether language or framework, the longer it takes to gain strength in programming skills. Now the question arises, 'Is the destination to
learn
everything or the journey there?'. You will probably never learn everything. So, for me, the destination is the journey. Why take a shortcut? Go
native.
History Log
- December 7, 2012 - Initial release. "A date that will live in infamy!" Franklin Delano Roosevelt.