最近接到一个任务,要让ClistCtrl像Excel一样可以编辑,经过一些摸索,最终实现效果如下。
当鼠标单击CListCtrl对象时,控件发出NM_CLICK消息,在这个消息的响应函数中,能够获取用户单击了哪个单元格;然后,动态创建一个CEdit控件,使其和单元格大小、字体、位置、文字完全相同;最后,当用户对编辑框操作完,焦点转向其他控件时,提取CEdit中文字内容,并更新到CListCtrl当中去。
为了集成上述功能,从CListCtrl派生一个自定义类CListCtrlEdit,具体实现代码如下(代码中另外实现了自动弹出提示框功能)。
//自定义列表控件头文件
//
#pragma once
class CListCtrlEdit : public CListCtrl
{
public:
CListCtrlEdit();
BOOL EnableTips(BOOL bEnableTips);
protected:
virtual BOOL PreTranslateMessage(MSG* pMsg);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg void OnNMClick(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnEnEdtKillFocus();
DECLARE_MESSAGE_MAP()
private:
CEdit m_wndEdt;
int m_iCurRow; //鼠标单击的行
int m_iCurColunm; //鼠标单击的列
int m_iHoverRow; //鼠标悬停的行
int m_iHoverColumn; //鼠标悬停的列
CToolTipCtrl m_wndToolTip; //提示框控件
BOOL m_bEnableTips; //鼠标移动到某个单元格时,是否给出提示
};
以下为自定义类的实现文件
#include "pch.h"
#include "CListCtrlEdit.h"
#define _ID_DYN_CREATE_EDIT_ 10000
CListCtrlEdit::CListCtrlEdit()
:m_iCurColunm(-1)
,m_iCurRow(-1)
,m_bEnableTips(FALSE)
,m_iHoverColumn(-1)
,m_iHoverRow(-1)
{}
BOOL CListCtrlEdit::EnableTips(BOOL bEnableTips)
{
EnableToolTips(TRUE);
//创建tooltip控件
m_bEnableTips = m_wndToolTip.Create(this, TTS_ALWAYSTIP);
if (m_bEnableTips)
{
m_wndToolTip.Activate(TRUE);
m_wndToolTip.SetDelayTime(TTDT_INITIAL, 500);
}
return m_bEnableTips;
}
BEGIN_MESSAGE_MAP(CListCtrlEdit, CListCtrl)
ON_NOTIFY_REFLECT(NM_CLICK, &CListCtrlEdit::OnNMClick)
ON_EN_KILLFOCUS(_ID_DYN_CREATE_EDIT_,&CListCtrlEdit::OnEnEdtKillFocus)
ON_WM_MOUSEMOVE()
END_MESSAGE_MAP()
void CListCtrlEdit::OnNMClick(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
//进行单击检测,这个结构已经被扩展为能够适应子项的单击检测。
LVHITTESTINFO cHitTest;
cHitTest.pt = pNMItemActivate->ptAction;
if (-1 != SubItemHitTest(&cHitTest)) //检测给定坐标位于哪个单元格上
{
if (cHitTest.flags & LVHT_ONITEMLABEL)
{
m_iCurRow = cHitTest.iItem;
m_iCurColunm = cHitTest.iSubItem;
//获取单元格信息
CRect rect; //单元格的尺寸
GetSubItemRect(m_iCurRow, m_iCurColunm, LVIR_LABEL, rect); //注意,这个坐标是相对于ListCtrl左上角的
CString sValue = GetItemText(m_iCurRow, m_iCurColunm); //获取当前单元格的内容
//动态创建编辑框
if (!m_wndEdt.m_hWnd)
{
m_wndEdt.Create(WS_CHILD | ES_CENTER | ES_AUTOHSCROLL | ES_WANTRETURN | ES_MULTILINE, rect, this, _ID_DYN_CREATE_EDIT_);
m_wndEdt.SetFont(this->GetFont(), FALSE); //设置编辑框的字体
}
m_wndEdt.SetWindowText(sValue);
m_wndEdt.MoveWindow(rect);
m_wndEdt.ShowWindow(SW_SHOW);
int iLength = sValue.GetLength();
m_wndEdt.SetSel(0, iLength, FALSE); //设置光标选中所有文字
m_wndEdt.SetFocus();
}
}
*pResult = 0;
}
void CListCtrlEdit::OnEnEdtKillFocus()
{
m_wndEdt.ShowWindow(SW_HIDE);
if (m_iCurColunm < 0 || m_iCurRow < 0)
return;
CString sEdt;
m_wndEdt.GetWindowText(sEdt);
SetItemText(m_iCurRow, m_iCurColunm, sEdt); //更新List Control的单元格显示
}
BOOL CListCtrlEdit::PreTranslateMessage(MSG* pMsg)
{
//为了让tool tip控件不错过重要消息,比如鼠标单击,必须将该消息转发给你的tool tip控件。最好的办法是在
//PreTranslateMessage函数中调用CToolTipCtrl::RelayEvent进行转发。
//以下为MSDN中原文:
//In order for the tool tip control to be notified of important messages, such as WM_LBUTTONXXX messages,
//you must relay the messages to your tool tip control. The best method for this relay is to make a call
//to CToolTipCtrl::RelayEvent, in the PreTranslateMessage function of the owner window.
//The following example illustrates one possible method (assuming the tool tip control is called m_ToolTip):
if (m_bEnableTips)
{
if (pMsg->message == WM_LBUTTONDOWN ||
pMsg->message == WM_LBUTTONUP ||
pMsg->message == WM_MOUSEMOVE)
{
m_wndToolTip.RelayEvent(pMsg);
}
}
return CListCtrl::PreTranslateMessage(pMsg);
}
void CListCtrlEdit::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
if (m_bEnableTips)
{
//判断当前鼠标所在的位置
CString str;
LVHITTESTINFO cHit;
cHit.pt = point;
SubItemHitTest(&cHit);
if (cHit.iItem != m_iHoverRow || cHit.iSubItem != m_iHoverColumn)
{
//保存当前鼠标所在的行、列
m_iHoverRow = cHit.iItem;
m_iHoverColumn = cHit.iSubItem;
if (m_iHoverRow != -1 && m_iHoverColumn != -1)
{
str = GetItemText(m_iHoverRow, m_iHoverColumn);
m_wndToolTip.AddTool(this, str);
m_wndToolTip.Pop(); //Removes a displayed tool tip window from the view.
}
else
{
m_wndToolTip.AddTool(this, _T("请单击某个单元格进行编辑"));
m_wndToolTip.Pop();
}
}
}
CListCtrl::OnMouseMove(nFlags, point);
}
在主对话框资源编辑器中添加一个CListCtrl控件,并在控件属性表中将其改为Report类型,最后在对话框头文件中将该控件类型修改为
public:
CListCtrlEdit m_wndLstEdit;
之后,在主对话框的OnInitDialog函数中完成对列表控件的初始化。
BOOL CEditableListCtrlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != nullptr)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
CRect rect;
m_wndLstEdit.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_ONECLICKACTIVATE);
m_wndLstEdit.GetClientRect(rect);
m_wndLstEdit.InsertColumn(0, _T("项目"), LVCFMT_RIGHT, rect.Width() / 2, 0);
m_wndLstEdit.InsertColumn(1, _T("值"), LVCFMT_CENTER, rect.Width() / 2, 1);
//这个地方特别注意,CListCtrl中第1列无法直接设置对齐样式,需要用以下方法
LVCOLUMN lvc;
lvc.mask = LVCF_FMT;
m_wndLstEdit.GetColumn(0, &lvc);
lvc.fmt &= ~LVCFMT_JUSTIFYMASK;
lvc.fmt |= LVCFMT_CENTER;
m_wndLstEdit.SetColumn(0, &lvc);
m_wndLstEdit.InsertItem(0, _T("河北"));
m_wndLstEdit.SetItemText(0, 1, _T("8"));
m_wndLstEdit.InsertItem(1, _T("山东"));
m_wndLstEdit.SetItemText(1, 1, _T("10"));
m_wndLstEdit.InsertItem(2, _T("河南"));
m_wndLstEdit.SetItemText(2, 1, _T(""));
m_wndLstEdit.EnableTips(TRUE);
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
Ok, that is all!