Midas
Midas

Reputation: 7131

Subclassing list view to edit only its subitems

I wrote this small program which displays a list view and makes items and subitems editable.

I want to change this to make only the subitems editable. And I would like to make the list view window procedure stand on itself, that I don't have to forward WM_NOTIFY messages every time as I'm doing now in WndProcMain. And the purpose is that I don't use only one list view with editable subitems in my program, I'm going to use it in many different windows.

The LVN_ENDLABELEDIT notification is processed by WndProcList because the bEditing has to be changed. This flag is used for WM_PAINT when subitems have to be edited. This is a fix, otherwise the text in the first subitem disappears because it thinks the first item is being edited. However, I would like to also receive a message like LVN_ENDLABELEDIT in the window procedure of the list view owner window (in this case WndProcMain), because I want to manipulate the user input also.

Please ask if you have questions.

Thanks in advance

Midas

WNDPROC     wpOrigEditProc;
RECT        rcSubItem;

LRESULT CALLBACK WndProcEditList(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_WINDOWPOSCHANGING:
            {
                WINDOWPOS *pos = (WINDOWPOS*) lParam;

                pos->x  = rcSubItem.left;
                pos->cx = rcSubItem.right;
            }
            break;
        default:
            return CallWindowProc(wpOrigEditProc, hWnd, uMsg, wParam, lParam);
    }
    return 1;
} 

LRESULT CALLBACK WndProcList(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    static HWND             hEdit;
    static RECT             rc;
    static LVITEM           lvI;
    static unsigned char    bEditing = 0;
    switch (uMsg) {
        case WM_NOTIFY:
            switch (((NMHDR*) lParam)->code) {
                case NM_CLICK:
                    lvI.iItem       = ((NMITEMACTIVATE*) lParam)->iItem;
                    lvI.iSubItem    = ((NMITEMACTIVATE*) lParam)->iSubItem;
                    break;
                case NM_DBLCLK:
                    SendMessage(hWnd, LVM_EDITLABEL, lvI.iItem, 0);
                    break;
                case LVN_BEGINLABELEDIT:
                    {
                        char    text[32];
                        bEditing        = 1;
                        hEdit           = (HWND) SendMessage(hWnd, LVM_GETEDITCONTROL, 0, 0);
                        rcSubItem.top   = lvI.iSubItem;
                        rcSubItem.left  = LVIR_LABEL;
                        SendMessage(hWnd, LVM_GETSUBITEMRECT, lvI.iItem, (long) &rcSubItem);
                        rcSubItem.right = SendMessage(hWnd, LVM_GETCOLUMNWIDTH, lvI.iSubItem, 0);
                        wpOrigEditProc  = (WNDPROC) SetWindowLong(hEdit, GWL_WNDPROC, (long) WndProcEditList);
                        lvI.pszText     = text;
                        lvI.cchTextMax  = 32;
                        SendMessage(hWnd, LVM_GETITEMTEXT, lvI.iItem, (long) &lvI);
                        SetWindowText(hEdit, lvI.pszText);
                    }
                    break;
                case LVN_ENDLABELEDIT:
                    bEditing = 0;
                    SetWindowLong(hEdit, GWL_WNDPROC, (long) wpOrigEditProc);
                    if (!lvI.iSubItem) return 1;
                    lvI.pszText = ((NMLVDISPINFO*) lParam)->item.pszText;
                    if (!lvI.pszText) return 1;
                    SendMessage(hWnd, LVM_SETITEMTEXT, lvI.iItem, (long) &lvI);
                    break;
                default:
                    return CallWindowProc((WNDPROC) GetClassLong(hWnd, GCL_WNDPROC), hWnd, uMsg, wParam, lParam);
            }
            break;
        case WM_PAINT:
            if (bEditing) {
                RECT rcItem;
                if (lvI.iSubItem > 0) {
                    rcItem.left = LVIR_LABEL;
                    if (SendMessage(hWnd, LVM_GETITEMRECT, lvI.iItem, (long) &rcItem))
                        ValidateRect(hWnd, &rcItem);
                }
            }
        default:
            return CallWindowProc((WNDPROC) GetClassLong(hWnd, GCL_WNDPROC), hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

LRESULT CALLBACK WndProcMain(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    static HWND hList;
    static RECT rc;
    switch (uMsg) {
        case WM_NOTIFY:
            switch (((NMHDR*) lParam)->code) {
                case NM_CLICK:
                case NM_DBLCLK:
                case LVN_BEGINLABELEDIT:
                case LVN_ENDLABELEDIT:
                    return CallWindowProc(WndProcList, ((NMHDR*) lParam)->hwndFrom, uMsg, wParam, lParam);
            }
            break;
        case WM_CREATE:
            {
                LVCOLUMN        lvc;
                LVITEM          lvI;
                unsigned int    i;
                float           vertex;
                char            text[32];

                hList = CreateWindow(WC_LISTVIEW, 0, WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_EDITLABELS, rc.left, rc.top, rc.right, rc.bottom, hWnd, 0, hInstance, 0);
                SendMessage(hList, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
                SetWindowLong(hList, GWL_WNDPROC, (long) WndProcList);

                lvc.mask    = LVCF_WIDTH;
                lvc.cx      = 30;
                SendMessage(hList, LVM_INSERTCOLUMN, 0, (LPARAM) &lvc);

                lvc.mask    = LVCF_TEXT;
                lvc.pszText = "Vertex";
                SendMessage(hList, LVM_INSERTCOLUMN, 1, (LPARAM) &lvc);
                SendMessage(hList, LVM_SETCOLUMNWIDTH, 1, LVSCW_AUTOSIZE_USEHEADER);

                lvI.mask    = LVIF_TEXT;
                lvI.pszText = text;

                for (i = 0; i < 10; i++) {
                    vertex = (float) i;
                    lvI.iItem       = i;
                    lvI.iSubItem    = 0;    
                    sprintf(text, "%d", i);
                    SendMessage(hList, LVM_INSERTITEM, 0, (LPARAM) &lvI);
                    lvI.iSubItem    = 1;    
                    sprintf(text, "%f, %f, %f", vertex - 1, vertex, vertex + 1);
                    SendMessage(hList, LVM_SETITEM, 0, (LPARAM) &lvI);
                }
            }
            break;
        case WM_SIZE:
            GetClientRect(hWnd, &rc);
            MoveWindow(hList, rc.left, rc.top, rc.right, rc.bottom, 1);
            SendMessage(hList, LVM_SETCOLUMNWIDTH, 1, LVSCW_AUTOSIZE_USEHEADER);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

Upvotes: 4

Views: 3905

Answers (2)

Cody Gray
Cody Gray

Reputation: 244732

The first problem that cropped up for me in trying to compile your code is the fact that you either aren't compiling for Unicode, or you're using the non-Unicode sprintf function to format text. That's the first thing that needs to be fixed: Windows applications have been fully Unicode for over a decade. Replace every instance of char variable declarations with wchar_t (or TCHAR), prefix your string literals with L (or surround them with the TEXT() macro), and quickly replace the calls to sprintf with calls to wsprintf. As the documentation indicates, there are certainly better functions to use than wsprintf, but the same is true for sprintf, and this gets the code to compile with minimum effort.

The other thing that looks non-idiomatic to me is your use of the Get/SetClassLong and Get/SetWindowLong functions. Nowadays, I always write code with 64-bit portability in mind, and therefore I'd replace those with the Get/SetClassLongPtr and Get/SetWindowLongPtr macros, which automatically resolve to the correct function call, depending on if you're compiling for x86 or x64. This isn't a deal-breaker, though.

You first asked if there was a way to handle the WM_NOTIFY message directly from the sub-classed control by forwarding them automatically. Unfortunately, this is not possible. The Win32 model is such that parents always own their children, and it is thus their responsibility to handle events. I agree with your intuition on the separation of concerns, but the only way to make this happen is to explicitly forward the message from the parent to the appropriate child control yourself. Frameworks like MFC (which encapsulate the Win32 API) do this for you apparently automatically, still have to forward the notification messages from the parent to the child. They do it using something called "message reflection", which you can read about here. There's nothing stopping you from implementing something similar in your own application, but at a certain point you have to stop and ask yourself if it isn't worth using one of the many available GUI frameworks just for this sort of thing.

Anyway, as I understand it, the main thrust of your question is this:

I want to change this to make only the subitems editable.

That seems like a pretty simple fix. All you have to do is check in the LVN_BEGINLABELEDIT notification message handler that the user has, in fact, requested to edit a sub-item. Since you've used it elsewhere in your code, you know that the LVITEM.iSubItem member gives you either the one-based index of the sub-item, or 0 if the structure refers to an item rather than a sub-item.

So insert this line to ensure that lvI.iSubItem is not equal to 0 at the top of the LVN_BEGINLABELEDIT handler:

if (lvI.iSubItem == 0) return TRUE;  // prevent editing

As the documentation for the LVN_BEGINLABELEDIT message indicates, returning FALSE allows the user to edit the label, and returning TRUE prevents them from editing. Since we return TRUE, we prevent the edit of anything but sub-items before the edit even starts.

It looks to me like you already tried to do something similar in the LVN_ENDLABELEDIT notification message handler with this line:

if (!lvI.iSubItem) return 1;

but that's too late! If the edit is already ending, then you've already given the user the impression that they were able to edit the main item, which you don't want to do. Take that line out, and it should work as expected.

Note that your implementation does have at least one glaring bug: you don't prevent the user from modifying the contents of a sub-item to a string longer than 32 characters, but your code populating the editing control only accepts a string up to 32 characters long:

TCHAR text[32];
// ... snip ...
lvI.pszText     = text;
lvI.cchTextMax  = 32;
SendMessage(hWnd, LVM_GETITEMTEXT, lvI.iItem, (long) &lvI);
SetWindowText(hEdit, lvI.pszText);

Writing that code the correct way would be (and I suspect that's why you haven't done it the right way) a giant pain in the ass. Typically, I will create a string buffer that I think is long enough, try to get the text of the sub-item, and check the return value of the LVM_GETITEMTEXT message. The return value tells me how many characters were copied into the string buffer. If the number of characters copied indicates that it entirely filled the available space in the string buffer, I'll make the buffer larger (perhaps doubling the size), and then try to send the LVM_GETITEMTEXT message again. As I recall, MFC does something similar. Told you it was a pain, but it's worth getting these things right.

A simpler solution (although more limiting) would be to prevent the user from ever setting the length of one of the sub-items to a string of text longer than 32 characters. Then you wouldn't have to worry about handling long input because you'd know it would never be there, and the user would never be confused about the behavior of your control. To do that, send the edit control an EM_LIMITTEXT message at the end of your LVN_BEGINLABELEDIT handler:

case LVN_BEGINLABELEDIT:
{
    // ... snip ...
    lvI.cchTextMax  = 32;
    SendMessage(hWnd, LVM_GETITEMTEXT, lvI.iItem, (long) &lvI);
    SetWindowText(hEdit, lvI.pszText);

    // (begin new code)
    SendMessage(hEdit, EM_LIMITTEXT, lvI.cchTextMax, 0);
}

Now the user can't enter more than the allowed number of characters, so your code knows that you will never have to deal with any more than that being there (unless you write code to place them there yourself, in which case...).


All of that said, I think I agree with Hans:

Ugh, you'll fight glitches forever. There's little point with grid controls universally available.

Upvotes: 1

King
King

Reputation: 1200

I posted an answer related to the question but it was in C#. Never had much of expertise in winapi rather than playing with it as a hobbyist long ago, The answer at the end of this post looks promising -- http://cboard.cprogramming.com/windows-programming/122733-%5Bc%5D-editing-subitems-listview-win32-api.html

Upvotes: 1

Related Questions