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.
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 ).
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.
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
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.
I use Visual Studio 2013, on Windows 7 x86;
I am developing in C++ using raw WinAPI;
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;
}
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(...);
}