c++clistviewwinapidatagrid

Properly handle subitem editing ( or canceling of subitem editing ) in listview with editable subitems


INTRODUCTION:

I am trying to implement listview control with editable subitems. For in-place editing of items/subitems I use edit control.

I believe that I have managed to properly code placing of the edit control above item/subitem.

PROBLEM:

I do not know on which events I should end/cancel subitem editing ( hide edit control, set subitem text etc ) and how should I do it.

To clarify, I speak of the moment when user finishes/cancels in place editing.

At this point edit control is no longer needed, so I should hide it ( I do not like recreating it every time; I believe that creating it once and then showing/hiding it when needed is more efficient ).

I am targeting the behavior Properties window has in Visual Studio ( see attached image to see exactly the window I refer to ).

enter image description here

I want to achieve editing/canceling the same way this window does when user presses ESC key/clicks on another window/clicks on scrollbar etc.

MY EFFORTS TO SOLVE THIS:

Using Google, I found few examples but they are old and do not address all of the relevant cases, so that is why I ask here for help.

However, I was able to find out that one of the events I must consider are EN_KILLFOCUS, case when user presses ESC/ENTER key and the case when user clicks on something other than edit control.

EDIT:

I have managed to handle ESC and ENTER keys, as well as the case when user clicks on the another sibling control or switches windows with ALT + TAB. I have updated SSCCE with relevant changes

QUESTION:

In order to achieve default behavior for a grid ( if there is one for Windows apps ), which messages/events must I handle?

Can you also point out where should I edit subitem and hide edit control, and where should I just hide edit control?

EDIT:

My only problem remained is to handle the case when user clicks on the listview scrollbars, or on the background of the main window. I just do not know how to handle this and would appreciate all the help I can get.

RELEVANT INFORMATION:

I use Visual Studio 2013, on Windows 7 x86;

I am developing in C++ using raw WinAPI;

SSCCE:

Below is the solution I have so far. I have tried to thoroughly comment it, but if more info is required leave a comment and I will update my post.

#include <windows.h>
#include <windowsx.h>   // various listview macros etc
#include <CommCtrl.h>
#include <stdio.h>      // swprintf_s()

// enable Visual Styles
#pragma comment( linker, "/manifestdependency:\"type='win32' \
                         name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
                         processorArchitecture='*' publicKeyToken='6595b64144ccf1df' \
                         language='*'\"")

// link with Common Controls library
#pragma comment( lib, "comctl32.lib") 

//global variables
HINSTANCE hInst;

// listview subclass procedure
LRESULT CALLBACK ListViewSubclassProc(HWND hwnd, UINT message, 
    WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    switch (message)
    {
    case WM_VSCROLL:
    case WM_HSCROLL:
        // if edit control has the focus take it away and give to listview
        if (GetFocus() == GetDlgItem(GetParent(hwnd), 5000))
            SetFocus(hwnd);  // use WM_NEXTDLGCTL for dialogbox !!!!
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass(hwnd, ListViewSubclassProc, uIdSubclass);
        return DefSubclassProc(hwnd, message, wParam, lParam);
    }
    return ::DefSubclassProc(hwnd, message, wParam, lParam);
}

// subclass procedure for edit control
LRESULT CALLBACK InPlaceEditControl_SubclassProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
    switch (message)
    {
    case WM_GETDLGCODE:
        return (DLGC_WANTALLKEYS | DefSubclassProc(hwnd, message, wParam, lParam));
    case WM_KILLFOCUS:
        ShowWindow(hwnd, SW_HIDE);
        return DefSubclassProc(hwnd, message, wParam, lParam);
    case WM_CHAR:
        //Process this message to avoid message beeps.
        switch (wParam)
        {
        case VK_RETURN:
            return 0L;
        case VK_ESCAPE:
            return 0L;
        default:
            return ::DefSubclassProc(hwnd, message, wParam, lParam);
        }
        break;
    case WM_KEYDOWN:
        switch (wParam)
        {
        case VK_RETURN:
        {
            // get listview handle
            HWND hwndLV = GetDlgItem(GetParent(hwnd), 2000);
            // get edit control's client rectangle
            RECT rc = { 0 };
            GetClientRect(hwnd, &rc);
            // since edit control lies inside item rectangle
            // we can test any coordinate inside edit control's
            // client rectangle
            // I chose ( rc.left, rc.top )
            MapWindowPoints(hwnd, hwndLV, (LPPOINT)&rc, (sizeof(RECT) / sizeof(POINT)));
            // get item and subitem indexes
            LVHITTESTINFO lvhti = { 0 };
            lvhti.pt.x = rc.left;
            lvhti.pt.y = rc.top;
            ListView_SubItemHitTest(hwndLV, &lvhti);
            // get edit control's text
            wchar_t txt[50] = L"";
            Edit_GetText(hwnd, txt, 50);
            // edit cell text
            ListView_SetItemText(hwndLV, lvhti.iItem, lvhti.iSubItem, txt);
            // restore focus to listview
            // this triggers EN_KILLFOCUS
            // which will hide edit control
            SetFocus(hwndLV);
        }
            return 0L;
        case VK_ESCAPE:
            SetFocus(GetDlgItem(GetParent(hwnd), 2000));
            return 0L;
        default:
            return ::DefSubclassProc(hwnd, message, wParam, lParam);
        }
        break;
    case WM_NCDESTROY:
        ::RemoveWindowSubclass(hwnd, InPlaceEditControl_SubclassProc, uIdSubclass);
        return DefSubclassProc(hwnd, message, wParam, lParam);

    }
    return ::DefSubclassProc(hwnd, message, wParam, lParam);
}
// main window procedure
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CREATE:
    {
        //================ create controls
        RECT rec = { 0 };
        GetClientRect(hwnd, &rec);

        HWND hwndLV = CreateWindowEx(0, WC_LISTVIEW,
            L"", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPSIBLINGS | LVS_REPORT, 
            50, 50, 250, 200, hwnd, (HMENU)2000, hInst, 0);
        // in place edit control
        HWND hwndEdit = CreateWindowEx(0, WC_EDIT, L"", ES_AUTOHSCROLL | WS_CHILD | WS_BORDER,
            200, 265, 100, 25, hwnd, (HMENU)5000, hInst, 0);
        // edit control must have the same font as listview
        HFONT hf = (HFONT)SendMessage(hwndLV, WM_GETFONT, 0, 0);
        if (hf)
            SendMessage(hwndEdit, WM_SETFONT, (WPARAM)hf, (LPARAM)TRUE);
        // subclass edit control, so we can edit subitem with ENTER, or
        // cancel editing with ESC
        SetWindowSubclass(hwndEdit, InPlaceEditControl_SubclassProc, 0, 0);
        // set extended listview styles
        ListView_SetExtendedListViewStyle(hwndLV, LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
        // subclass listview
        SetWindowSubclass(hwndLV, ListViewSubclassProc, 0, 0);

        // add some columns
        LVCOLUMN lvc = { 0 };

        lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
        lvc.fmt = LVCFMT_LEFT;

        for (long nIndex = 0; nIndex < 5; nIndex++)
        {
            wchar_t txt[50];
            swprintf_s(txt, 50, L"Column %d", nIndex);

            lvc.iSubItem = nIndex;
            lvc.cx = 60;
            lvc.pszText = txt;

            ListView_InsertColumn(hwndLV, nIndex, &lvc);
        }

        // add some items
        LVITEM lvi;

        lvi.mask = LVIF_TEXT;

        for (lvi.iItem = 0; lvi.iItem < 10000; lvi.iItem++)
        {
            for (long nIndex = 0; nIndex < 5; nIndex++)
            {
                wchar_t txt[50];
                swprintf_s(txt, 50, L"Item %d%d", lvi.iItem, nIndex);

                lvi.iSubItem = nIndex;
                lvi.pszText = txt;

                if (!nIndex)  // item 
                    SendDlgItemMessage(hwnd, 2000, LVM_INSERTITEM, 0, reinterpret_cast<LPARAM>(&lvi));
                else          // sub-item
                    SendDlgItemMessage(hwnd, 2000, LVM_SETITEM, 0, reinterpret_cast<LPARAM>(&lvi));
            }
        }
    }
        return 0L;
    case WM_NOTIFY:
    {
        if (((LPNMHDR)lParam)->code == NM_DBLCLK)  
        {
            switch (((LPNMHDR)lParam)->idFrom)
            {
            case 2000: // remember, this was our listview's ID
            {
                LPNMITEMACTIVATE lpnmia = (LPNMITEMACTIVATE)lParam;

                // SHIFT/ALT/CTRL/their combination, must not be pressed
                if ((lpnmia->uKeyFlags || 0) == 0)
                {
                    // store item/subitem rectangle
                    RECT rc = { 0, 0, 0, 0 };
                    // helper values, needed for handling partially visible items
                    int topIndex = ListView_GetTopIndex(lpnmia->hdr.hwndFrom);
                    int visibleCount = ListView_GetCountPerPage(lpnmia->hdr.hwndFrom);
                    // if item is vertically partially visible, make it fully visible
                    if ((topIndex + visibleCount) == lpnmia->iItem)
                    {
                        // get the rectangle of the above item -> lpnmia->iItem - 1
                        ListView_GetSubItemRect(lpnmia->hdr.hwndFrom, lpnmia->iItem - 1, lpnmia->iSubItem, LVIR_LABEL, &rc);
                        // ensure clicked item is visible
                        ListView_EnsureVisible(lpnmia->hdr.hwndFrom, lpnmia->iItem, FALSE);
                    }
                    else // item is fully visible, just get its ectangle
                        ListView_GetSubItemRect(lpnmia->hdr.hwndFrom, lpnmia->iItem, lpnmia->iSubItem, LVIR_LABEL, &rc);

                    RECT rcClient = { 0 };  // listview client rectangle, needed if item partially visible
                    GetClientRect(lpnmia->hdr.hwndFrom, &rcClient);
                    // item is horizontally partially visible -> from the right side
                    if (rcClient.right < rc.right)  
                    {
                        // show the whole item
                        ListView_Scroll(lpnmia->hdr.hwndFrom, rc.right - rcClient.right, 0);
                        // adjust rectangle so edit control is properly displayed
                        rc.left -= rc.right - rcClient.right;
                        rc.right = rcClient.right;
                    }
                    // item is horizontally partially visible -> from the left side
                    if (rcClient.left > rc.left)  
                    {
                        // show the whole item
                        ListView_Scroll(lpnmia->hdr.hwndFrom, rc.left - rcClient.left, 0);
                        // adjust rectangle so edit control is properly displayed
                        rc.right += rcClient.left - rc.left;
                        rc.left = rcClient.left;
                    }
                    // it is time to position edit control, we start by getting its window handle
                    HWND hwndEdit = GetDlgItem(hwnd, 5000);
                    //  get item text and set it as edit control's text
                    wchar_t text[51];
                    ListView_GetItemText(lpnmia->hdr.hwndFrom, lpnmia->iItem, lpnmia->iSubItem, text, 50);
                    Edit_SetText(hwndEdit, text);
                    // select entire text
                    Edit_SetSel(hwndEdit, 0, -1);
                    // map listview client rectangle to parent rectangle
                    // so edit control can be properly placed above the item
                    MapWindowPoints(lpnmia->hdr.hwndFrom, hwnd, (LPPOINT)&rc, (sizeof(RECT) / sizeof(POINT)));
                    // move the edit control
                    SetWindowPos(hwndEdit, HWND_TOP, rc.left, rc.top, rc.right - rc.left, 
                        rc.bottom - rc.top, SWP_SHOWWINDOW);
                    // set focus to our edit control
                    HWND previousWnd = SetFocus(hwndEdit);
                }
            }
                break;
            default:
                break;
            }
        }
    }
        break;
    case WM_CLOSE:
        ::DestroyWindow(hwnd);
        return 0L;
    case WM_DESTROY:
    {
        ::PostQuitMessage(0);
    }
        return 0L;
    default:
        return ::DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

// WinMain
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,
    int nCmdShow)
{
    // store hInstance in global variable for later use
    hInst = hInstance;

    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    // register main window class
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInst;
    wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = GetSysColorBrush(COLOR_WINDOW);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"Main_Window";
    wc.hIconSm = LoadIcon(hInstance, IDI_APPLICATION);

    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"Window Registration Failed!", L"Error!", 
            MB_ICONEXCLAMATION | MB_OK);

        return 0;
    }

    // initialize common controls
    INITCOMMONCONTROLSEX iccex;
    iccex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    iccex.dwICC = ICC_LISTVIEW_CLASSES | ICC_STANDARD_CLASSES;
    InitCommonControlsEx(&iccex);

    // create main window
    hwnd = CreateWindowEx(0, L"Main_Window", L"Grid control",
        WS_OVERLAPPEDWINDOW, 50, 50, 400, 400, NULL, NULL, hInstance, 0);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

Solution

  • Update:

    On second thought, the method I posted earlier was wrong. I think it's design error to use SetCapture in edit-box like that, it can interfere with bunch of other things. I am going to delete my old answer and pretend nobody saw it!

    Your own method is fine with checking for KILLFOCUS, you just need subclass for ListView to check scroll messages to mimic LVN_XXXLABELEDIT

    void hideEdit(BOOL save)
    {
        //save or not...
        ShowWindow(hedit, SW_HIDE);
    }
    
    LRESULT CALLBACK EditProc...
    {
        if (msg == WM_KILLFOCUS)
            hideEdit(1);
    
        if (msg == WM_CHAR)
        {
            if (wParam == VK_ESCAPE){
                hideEdit(0);
                return 0;
            }
            if (wParam == VK_RETURN){
                hideEdit(1);
                return 0;
            }
        }
    
        return DefSubclassProc(...);
    }
    
    LRESULT CALLBACK ListProc...
    {
        if (msg == WM_VSCROLL || msg == WM_HSCROLL) hideEdit(1);
        return DefSubclassProc(...);
    }