Log in

View Full Version : Editable Listview control


ZaiRoN
November 6th, 2007, 07:20
Few days ago I started renewing my PE editor's gui, I wanted to replace some edit box controls with a listview control. Everything was going well until I had to edit the value inside a cell. With the original listview control you can change the text of the first subitem of a row only, but I would like to edit every single subitem. How can I solve the problem? I didn't want to waste time solving the problem so I decided to take a look at the usual programming places starting from Code Project. Hm, nothing. I'm not so good in searching information through the net, but seems like there are working samples on mfc, .net and vb only. No win32 programming stuff... Well, I decided to give it a try subclassing the control.
I have never subclass-ed a control before, it's my first try. I don't know if there's a better approach. I don't even know if it's the correct way to solve the problem, but it seems to works well. Let's start.

The steps to follow are:
1. Create an edit box that will be used to insert the new text
2. Set the new window procedure able to handle edit control's messages
3. Apply/abort text modification

I use VS creating a win32 project. Add a listview control to your dialog setting "Edit labels" to FALSE. If you set the option to TRUE the OS will handle subitem modification, I prefer to avoid this behaviour.

The idea is to change the subitem's text when a double click occours. I catch the event in the main window procedure calling the function (named SubClass_ListView_Editable) which subclasses the control.

The first step consists of creating the edit box over the clicked subitem. To create the edit box I need to know where to put it. LVM_GETSUBITEMRECT returns information about the rectangle for a subitem of a listview control. With this information I can create the new control:
Code:
ListView_GetSubItemRect(hListView, _lParam->iItem, _lParam->iSubItem, LVIR_LABEL, &r);
// Time to create the new edit box
hEditable = CreateWindowEx(0, "EDIT", "Edit me", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | ES_MULTILINE, r.left, r.top, r.right-r.left, r.bottom-r.top, _lParam->hdr.hwndFrom, NULL, hInst, 0);

"r" is defined as a RECT structure:

Code:
typedef struct _RECT {
LONG left; // x-coordinate of the upper-left corner of the rectangle
LONG top; // y-coordinate of the upper-left corner of the rectangle
LONG right; // x-coordinate of the lower-right corner of the rectangle
LONG bottom; // y-coordinate of the lower-right corner of the rectangle
} RECT, *PRECT;

As you can see I use "r" inside CreateWindowEx function specifying the coordinates of the new control.
The third parameter of CreateWindowEx is the text that will be shown in the control. I use a static text but you can leave it blank or display the subitem's text, it's up to you. Now, the new control has been created and I'm going to set some features:

Code:
SendMessage(hEditable, EM_LIMITTEXT, 8, 0); // It accepts no more than 8 chars
SendMessage(hEditable, EM_SETSEL, 0, 8); // Text selected
SetFocus(hEditable); // Focus to the new box

If you don't need a particular behaviour (limit text, accept only numbers...) you can avoid the first two calls but I think the third one is useful, it gives the focus to the new edit box.

The control is complete, I have to add the new window procedure. This can be done using SetWindoLong function:

Code:
LONG SetWindowLong(
HWND hWnd, // Handle of the new edit control
int nIndex, // The attribute to change
LONG dwNewLong // The new value
);

The function changes an attribute of a specified window, in this case I'm going to change the address of the dialog procedure. The aim is to add a new window procedure for handling edit box's messages only. I'll pass over all the other messages forwarding them towards the old window procedure.

Code:
wpOld = (WNDPROC)SetWindowLong(hEditable, GWL_WNDPROC, SubClass_ListView_WndProc);
SetProp(hEditable, "WP_OLD", (HANDLE)wpOld);

SubClass_ListView_WndProc represents the new dialog procedure.
I have to save the address of the original window procedure because I have to restore it when I'll destroy the edit box. To save the address I use SetProp function, but if you prefer you can use global variables. To end this piece of code I save some more useful information: row and column of the subitem to change:

Code:
SetProp(hEditable, "ITEM", (HANDLE)_lParam->iItem);
SetProp(hEditable, "SUBITEM", (HANDLE)_lParam->iSubItem);

Which kind of messages will I have to catch? WM_KEYDOWN and WM_DESTROY only, all the other messages are passed to the original window procedure in this way:

Code:
return CallWindowProc((WNDPROC)GetProp(hEditable, "WP_OLD", hwnd, uMsg, wParam, lParam);

I catch WM_KEYDOWN because I have to handle ENTER ans ESC key. When you hit ENTER the text will be saved in the subitem, and when you click ESC the operation will be aborted:

Code:
case WM_KEYDOWN:
if (LOWORD(wParam) == VK_RETURN)
{
...
// Item and suibtem to change
LvItem.iItem = GetProp(hEditable, "ITEM";
LvItem.iSubItem = GetProp(hEditable, "SUBITEM";
// Where to store the new text
LvItem.pszText = text;
// Get new text and set it in the subitem
GetWindowText(hEditable, text, sizeof(text));
SendMessage(hListView, LVM_SETITEMTEXT, (WPARAM)GetProp(hEditable, "ITEM", (LPARAM)&LvItem);
DestroyWindow(hEditable);
}
else if (LOWORD(wParam) == VK_ESCAPE)
DestroyWindow(hEditable);
break;

If you press ESC the edit box will be destroyed without changing the subitem's text.
When ENTER is pressed I simply get the text storing it in the subitem. After that I can destroy the edit box.
There's something more to say about VK_RETURN. Look at CreateWindowEx parameters, there's a ES_MULTILINE value. The value is necessary otherwise the application refuses to catch ENTER.
The last thing I check in the new window procedure is WM_DESTROY message, I simply remove the saved properties and then I restore the original window procedure calling SetWindowLong again.

What about mouse click when I'm changing the subitem's value? It's like VK_ESCAPE key, I remove the edit box aborting the operation. See the attached source code for details.

The picture represents the subclassing method in action:


http://zairon.wordpress.com/files/2007/11/listview-subclass.jpg

The code can be optimized for sure, just fix/change/remove/add everything you need.
Feel free to post your comment/suggestion/criticism here.
Download the VS project from http://www.box.net/shared/static/kcurtvm8v7.zip

Game over!

dELTA
November 6th, 2007, 13:15
Nice tutorial ZaiRoN, appreciated as always.

JMI
November 6th, 2007, 14:00
And thanks for sharing with out Readers.

Regards,

begemott
November 7th, 2007, 09:42
Hello Zairon,

I don't know the reason why you have chosen this way to write your app. You could make use of C++, WTL(even MFC)....
If you don't believe THIS is the right way to write huge programs then it is OK. (I hope asm fans are not reading this )
Having said this, let me say a few words about the presented solution.

1. Using an edit box to edit all the fields in a listview is the correct and commonly used approach

2. Subclassing is a very powerful technique used for many purposes. But for your purpose it is still possible to do your job without it. How?

- Take into account the following:
If a dialog box or one of its controls currently has the input focus, then pressing the ENTER key causes Windows to send a WM_COMMAND message with the idItem (wParam) parameter set to the ID of the default command button. If the dialog box does not have a default command button, then the idItem parameter is set to IDOK by default.

When an application receives the WM_COMMAND message with idItem set to the ID of the default command button, the focus remains with the control that had the focus before the ENTER key was pressed. Calling GetFocus() at this point returns the handle of the control that had the focus before the ENTER key was pressed. The application can check this control handle and determine whether it belongs to any of the edit controls in the dialog box.

- Remove the ES_MULTILINE style, because you don't want to stop the above mechanism

- If you need the EN_*** notifications in your DialogProc, make the dialog the parent of the edit box.

Got the idea?
(If not PM me since this is a reversing, not a programming forum and I don't want to torture forum members with my english )

Regards,
begemott

ZaiRoN
November 7th, 2007, 17:52
Hello begemott.
thanks for your comment.
Quote:
Using an edit box to edit all the fields in a listview is the correct and commonly used approach
I don't want another edit box in the dialog, I wanted to edit fields on the fly. That's why I subclass...

begemott
November 9th, 2007, 08:19
Hello Zairon,

Please find attached my changes to main.cpp. It illustrates what I would like to say.

Regards!
begemott

ZaiRoN
November 9th, 2007, 08:34
Hello begemott.
Ah, now I understand what you wanted to say. Yes, you are right saying subclassing is useless. Thank you very much

LLXX
November 10th, 2007, 04:54
If the listview was larger and had scrollbars, your edit control wouldn't move with the cell and just stay there floating...

More seriously, this:

Doubleclick on item (0,0).
Click on item (1,0).
Receive exception:
Code:
EDITABLE_LISTVIEW caused an invalid page fault in
module <unknown> at 0084:00000000.
Registers:
EAX=00000000 CS=016f EIP=00000000 EFLGS=00010286
EBX=0063f246 SS=0177 ESP=0063f1a8 EBP=0063f214
ECX=0040104c DS=0177 ESI=0000826a FS=3c87
EDX=80007b90 ES=0177 EDI=0063f1fc GS=0000
Bytes at CS:EIP:
00 00 00 00 65 04 70 00 16 00 cd 06 00 80 00 00
Stack dump:
0040104c 000008f8 00000082 00000000 00000000 0000826a 0063f268 0063f234 00401175 000008f8 fffffffc 00000000 00000007 0040118b 10e90000 00000903
EIP=00000000...?

I think the reason why you don't find articles on it is because it's simpler to have a separate edit box in the dialog and use that to change the entries in the ListView instead of trying to superimpose an edit control onto a ListView entry. Even M$ Excel doesn't use a ListView.

ZaiRoN
November 10th, 2007, 05:32
The exception doesn't occur here... what's the OS you are running on?

LLXX
November 11th, 2007, 08:00
Win98SE. I suppose your OS (XP? 2000?) is being more "lenient" in handling the exception, i.e. ignores it and continues or something (leading you to the impression that nothing wrong happened)

Try checking the code at 0040104c -- that's the last address inside your app indicated by the stack dump, and corresponds to a "pop esi" instruction:
Code:
00401046 FF15F8604000 call CallWindowProcA
0040104C 5E pop esi