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); result = wcslen(text); if (_wcsnicmp(startInDir, text, result) == 0 )
{ if (tvi.cChildren > 0 )
{
if (!(tvi.state & TVIS_EXPANDEDONCE ))
{ TreeView_Expand(hTree, tvi.hItem, TVE_EXPAND); return 0; }
else
{ 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
{ return 0;
}
}
else
{
nmtvi = TreeView_GetNextSibling(hTree,nmtvi); }
}
}
} } } } } else
{
return 0; }
return 0;
} }
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); }
wsprintf(nodeCD, L"%s%s", nodeCD,tvi.pszText); getDirectories(tvi.hItem, nodeCD);
nmtvi = TreeView_GetNextSibling(hTree, tvi.hItem);
}
}
}
return false;
} break;
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 ))
{ Sleep(300);
TreeView_Expand(hTree, tvi.hItem, TVE_EXPAND); return false; }
}
}
}
nmtvi = TreeView_GetNextSibling(hTree, tvi.hItem);
} } } } return false;
} break;
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(" :\\"); TCHAR szTemp[BUFSIZE];
hti = (HTREEITEM)TVI_ROOT; if (GetLogicalDriveStrings(BUFSIZE-1, szTemp))
{
TCHAR* p = szTemp;
TCHAR* d = szDTemp;
do
{
*d = *p;
nodeParent = AddItemToTree(hwndTV, hti, szDTemp);
getDirectories(nodeParent, szDTemp);
if (hti == NULL)
return FALSE;
if (NULL == nodeParent)
{
return FALSE;
}
else if (*d == *startInDir)
{
TreeView_Expand(hTree, nodeParent, TVE_EXPAND);
}
while (*p++); }
while (*p);
}
else
return FALSE;
nodeParent = TreeView_GetSelection(hTree);
TreeView_Select(hTree,nodeParent,TVGN_FIRSTVISIBLE);
return TRUE;
}
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.