扩展CListCtrl编辑框功能
2024-03-17 17:33:41

公共控件 ListView 内置了编辑框功能,但是有个致命的缺陷:只能编辑第一列,第二列后都无法编辑。
为了能编辑任意单元格,所以我们需要扩展它。

编辑框基本用法

控件窗口添加 LVS_EDITLABELS 样式

1
2
3
DWORD dwStyle = _list.GetStyle();
dwStyle |= LVS_EDITLABELS;
_list.ModifyStyle(0, LVS_EDITLABELS);

然后就可以调用其成员方法 EditLabel 了。
相应的,取消编辑框使用 CancelEditLabel 方法。

ListView 内部处理过程

当用户调用 EditLabel 方法时,本质就是向控件发送一个 LVM_EDITLABEL 消息,接着内部就创建一个 Edit 控件。(向控件发送 LVM_GETEDITCONTROL 消息可以获得控件句柄)

EditLabel 执行过程中,会向其自身发送一个 LVN_BEGINLABELEDIT 消息,预示着编辑框即将显示。
当编辑框失去焦点或编辑完成时,ListView 会收到 LVN_ENDLABELEDIT 消息,表示编辑工作结束。

解决方案

网上有两种解决方案,一个是子类化Edit控件,就像这两篇文章中介绍的方法
Editing listview subitems using LVM_GETEDITCONTROL
Simplified Subitem Editing
测试发现有些弊端,比如在 WinCE 系统上没有 ON_WM_WINDOWPOSCHANGING 消息,得在其他地方改变 Edit 控件的位置。还有一个绘制问题,ListView 内部实现会使第一列内容区无效(表现为空白,可能是为了提高绘图效率),待编辑框结束后才会显示。

第二种方法就是自己创建一个 Edit 控件,这样就没有第一种方法的一些问题,实现更简单。
网上看到的方案都是扩展 ListView 控件,内部在单击或双击时显示 Edit 控件,完全不理会 MFC 提供的 EditLabel 等方法。
我稍微变化一下,自己处理几个相关的消息,这样的好处是兼容了 MFC 的接口,对外是透明的。
根据前面提到的知识点,源头是从 LVN_BEGINLABELEDIT 消息开始的,所以我们需要”劫持”这个消息

1
2
3
BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
ON_NOTIFY_REFLECT_EX(LVN_BEGINLABELEDIT, OnBeginLabelEdit)
END_MESSAGE_MAP()

这里要做的就是将 Edit 移动到合适的位置,并填充文本内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL CListCtrlEx::OnBeginLabelEdit(LPNMHDR pNMHDR, LRESULT* pResult)
{
// NMLVDISPINFO* pItem = reinterpret_cast<NMLVDISPINFO*>(pNMHDR);

_edit.SetWindowText(GetItemText(_nCurRow, _nCurCol));
CRect rc;
if (GetSubItemRect(_nCurRow, _nCurCol, LVIR_LABEL, rc)) {
_edit.MoveWindow(rc);
_edit.ShowWindow(SW_SHOW);
_edit.SetSel(_edit.LineLength());
_edit.SetFocus();
}

// 关键的两句,表示"已处理"该消息,这样就屏蔽了 ListView 控件的默认行为
*pResult = TRUE;
return TRUE;
}

接着要考虑编辑框按下回车或ESC键时,不要让父窗口关闭了(基于对话框的程序)

1
2
3
4
5
6
7
8
9
10
BOOL CEditEx::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->message == WM_KEYDOWN) {
if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE) {
PostMessage(WM_KILLFOCUS);
return TRUE;
}
}
return __super::PreTranslateMessage(pMsg);
}

CListCtrlEx 还要监视编辑框的退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
ON_EN_KILLFOCUS(_IDC_EDIT, OnEditKillFocus)
END_MESSAGE_MAP()

void CListCtrlEx::OnEditKillFocus()
{
NMLVDISPINFO info = { 0 };
info.hdr.hwndFrom = m_hWnd;
info.hdr.idFrom = _IDC_EDIT;
info.hdr.code = LVN_ENDLABELEDIT;
info.item.iItem = _nCurRow;
info.item.iSubItem = _nCurCol;
SendMessage(WM_NOTIFY, _IDC_EDIT, reinterpret_cast<LPARAM>(&info));
}

最后在 LVN_ENDLABELEDIT 处理过程中将编辑后的内容写回对应的单元格

1
2
3
4
5
6
7
8
9
10
11
12
BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
ON_NOTIFY_REFLECT_EX(LVN_ENDLABELEDIT, OnEndLabelEdit)
END_MESSAGE_MAP()

BOOL CListCtrlEx::OnEndLabelEdit(LPNMHDR pNMHDR, LRESULT* pResult)
{
_edit.ShowWindow(SW_HIDE);
CString value;
_edit.GetWindowText(value);
SetItemText(_nCurRow, _nCurCol, value);
return FALSE;
}