可编辑子项的CListCtrl类

一、说明

大家都知道在MFC中通过给CListCtrl设置LVS_EDITLABELS属性,并且在程序中响应控件的LVN_ENDLABELEDIT消息可以修改列表控件每一行的第一项,也就是主项(Item)。代码如下:

void CEditListCtrlSampleDlg::OnEndlabeleditList1(NMHDR* pNMHDR, LRESULT* pResult)
{
  LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
  // TODO: Add your control notification handler code here

  *pResult = TRUE;  //TRUE值表示可以修改主项,FALSE值表示不修改主项
}

但是让人郁闷的是,微软留了一手,CListCtrl不支持直接修改子项(SubItem)。无奈之下只好自力更生,对CListCtrl进行扩展。>_<!!!!

二、原理

通过在浩如烟海的互联网上查找资料(当然包括了大名鼎鼎的VCKBASE),发现现有的实现大都是对子项鼠标单击一次就可以编辑。但本人对CListCtrl的单击一次高亮文本,再单击一次才开始编辑的操作模式感觉比较喜欢,所以就有了这篇文章的诞生。

要想实现高亮文本也就是对文本进行着色处理,这可以通过对NM_CUSTOMDRAW消息进行处理实现,但是类向导中没有这个消息映射只能进行手工添加。

要想编辑文本则可以通过EditLabel(int nItem)成员函数以及对LVN_BEGINLABELEDIT和LVN_ENDLABELEDIT的消息处理实现。

三、实现

本文最终实现的CEditListCtrl扩展类在尽量符合CListCtrl操作步骤的情况下实现对主项及子项的可编辑。

成员变量说明:

int m_iItem; //主项标识符

int m_iSubItem; //子项标识符

BOOL m_bFocus; //是否绘制项文本焦点框

BOOL m_bHighLight; //是否高亮项文本

CItemEdit m_edtItemEdit; //用于子类化EditLabel函数返回的CEdit*指针

列表控件中所有项文本的绘制以及特效(焦点框、高亮)都在NM_CUSTOMDRAW消息处理中实现:

void CEditListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult)
{
  NMLVCUSTOMDRAW* pNMLVCustomDraw = (NMLVCUSTOMDRAW*)pNMHDR;

  // Take the default processing unless we set this to something else below.
  *pResult = CDRF_DODEFAULT;

  // First thing - check the draw stage. If it's the control's prepaint
  // stage, then tell Windows we want messages for every item.

  if (pNMLVCustomDraw->nmcd.dwDrawStage == CDDS_PREPAINT)
  {
    *pResult = CDRF_NOTIFYITEMDRAW;
  }
  else if (pNMLVCustomDraw->nmcd.dwDrawStage == CDDS_ITEMPREPAINT)
  {
    // This is the notification message for an item. We'll request
    // notifications before each subitem's prepaint stage.
    *pResult = CDRF_NOTIFYSUBITEMDRAW;
  }
  else if (pNMLVCustomDraw->nmcd.dwDrawStage == (CDDS_ITEMPREPAINT | CDDS_SUBITEM))
  {
    //当前要绘制的主项标识符和子项标识符
    int iItem = (int)pNMLVCustomDraw->nmcd.dwItemSpec;
    int iSubItem = pNMLVCustomDraw->iSubItem;

    CDC* pDC = CDC::FromHandle(pNMLVCustomDraw->nmcd.hdc);

    CString strItemText = GetItemText(iItem, iSubItem);
    CRect rcItem, rcText;
    GetSubItemRect(iItem, iSubItem, LVIR_LABEL, rcItem);
    rcText = rcItem;

    CSize size = pDC->GetTextExtent(strItemText);
    if(strItemText == _T(""))
    {
      size.cx = 41;
    }

    //设置文本高亮矩形
    rcText.left += 4;
    rcText.right = rcText.left + size.cx + 6;
      if(rcText.right > rcItem.right)
  {
    rcText.right = rcItem.right;
  }

  COLORREF crOldTextColor = pDC->GetTextColor();
  //绘制项焦点/高亮效果
  if(m_bFocus)
  {
    if((m_iItem == iItem) && (m_iSubItem == iSubItem))
    {
      if(m_bHighLight)
      {
        pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
        pDC->FillSolidRect(&rcText, ::GetSysColor(COLOR_HIGHLIGHT));
      }
      pDC->DrawFocusRect(&rcText);
    }
  }

  //绘制项文本
  rcItem.left += 6;
  pDC->DrawText(strItemText, &rcItem, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOCLIP);
  pDC->SetTextColor(crOldTextColor);
  *pResult = CDRF_SKIPDEFAULT;// We've painted everything.
  }
}

 

 

 

 

 

 

单击一次文本高亮,再单击一次文本开始编辑在WM_LBUTTONDOWN消息处理中实现:

void CEditListCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
  m_bFocus = TRUE;
  LVHITTESTINFO lvhit;
  lvhit.pt = point;
  int item = SubItemHitTest(&lvhit);

  //if (over a item/subitem)
  if (item != -1 && (lvhit.flags & LVHT_ONITEM))
  {
    CListCtrl::OnLButtonDown(nFlags, point);

    if(m_bHighLight && m_iItem == lvhit.iItem && m_iSubItem == lvhit.iSubItem)
    {
      //第二次单击
      EditLabel(m_iItem);
      return;
    }
    else
    {
      //第一次单击
      m_iItem = lvhit.iItem;
      m_iSubItem = lvhit.iSubItem;
      m_bHighLight = TRUE;
    }
  }
  else
  {
    if(m_edtItemEdit.m_hWnd == NULL)
    {
      //未出现文本编辑框时
      m_bHighLight = FALSE;
    }

    CListCtrl::OnLButtonDown(nFlags, point);
  }

  Invalidate(); //强制重绘控件
}

关键的一步,对项文本进行编辑。在以上代码中当执行到EditLabel时将会产生一个编辑框,这时需要将它进行子类化处理,以控制它出现的位置。

void CEditListCtrl::OnBeginlabeledit(NMHDR* pNMHDR, LRESULT* pResult)
{
  LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
  if (m_iSubItem >= 0)
  {
    ASSERT(m_iItem == pDispInfo->item.iItem);
    CRect rcSubItem;
    GetSubItemRect( pDispInfo->item.iItem, m_iSubItem, LVIR_BOUNDS, rcSubItem);

    //get edit control and subclass
    HWND hWnd= (HWND)SendMessage(LVM_GETEDITCONTROL);
    ASSERT(hWnd != NULL);
    VERIFY(m_edtItemEdit.SubclassWindow(hWnd));
    //move edit control text 4 pixel to the right of org label,
    //as Windows does it...编辑框定位
    m_edtItemEdit.m_iXPos = rcSubItem.left + 4;
    m_edtItemEdit.SetWindowText(GetItemText(pDispInfo->item.iItem, m_iSubItem));
  }
  *pResult = 0;
}
void CEditListCtrl::OnEndlabeledit(NMHDR* pNMHDR, LRESULT* pResult)
{
  LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
  LV_ITEM  *plvItem = &pDispInfo->item;

  if (m_iSubItem >= 0)
  {
    if (plvItem->pszText != NULL )
    {
      SetItemText(plvItem->iItem,m_iSubItem, plvItem->pszText);
    }

    VERIFY(m_edtItemEdit.UnsubclassWindow()!=NULL);
    *pResult = 0;
  }
  //编辑文本时对控件父窗口操作(如单击其它控件)引发"OnEndlabeledit"时刷新控件
  CRect rect;
  GetWindowRect(&rect);
  CPoint point;
  ::GetCursorPos(&point);
  if(!rect.PtInRect(point))
  {
    m_iItem = -1;
    m_iSubItem = -1;
    m_bFocus = FALSE;
    m_bHighLight = FALSE;
  }
}

通过以上三个步骤大体实现了本文要达到的目的,但是还不能放松。接下来还要进行一些显示细节方面的处理。这包括对WM_PAINT、WM_SETFOCUS和WM_KILLFOCUS消息的处理,限于篇幅,就不进行细讲了,有兴趣的朋友可以查看本文提供的源代码。最后实现的效果如下图所示:

 

 

一、在资源编辑器中建立一个菜单资源
  新建一个菜单资源,比如把菜单的ID号为IDC_POPMENU。此菜单有一项两层,即有一个可弹出的菜单项,而此菜单项的弹出内容即为将要建立的弹出式菜单的内容。至于每一个菜单项的消息映射,和一般的菜单相同。

      例如:新建弹出菜单IDR_POPMENU

      PopMenu-> Item1

                       -> Item2

                       -> Item3

    在这里,真正在右键单击时,弹出的是 Item1 , Item2 , 和 Item3 ,最外层的 PopMenu 是不显示的,因此,弹出菜单至少要有这两层。


二、使用CMenu类对象
  CMenu类的成员函数较多,但建立弹出式菜单只需用到其中几个成员函数。
1、LoadMenu函数
功能:从应用程式的可执行文档中加载菜单资源
  原型:BOOL LoadMenu( UINT nIDResource );
  其中nIDResource是菜单资源的ID号,这里用的是刚建立的IDC_POPMENU。

2、GetSubMenu函数
功能:此函数用于得到子菜单的指针。
  原型:CMenu* GetSubMenu( int nPos ) const;
  nPos为层数,0为第一层子菜单……以此类推。
由于我们需要的是“可弹出项”的第一层子菜单,因此用GetSubMenu(0)来得到第一层子菜单的类指针。


3、TrackPopupMenu函数
功能:在指定位置显示弹出菜单,并跟踪所选择的项
  原型:BOOL TrackPopupMenu( UINT nFlags,int x,int y,CWnd* pWnd,LPCRECT lpRect = NULL );
  其中:
nFlags为屏幕坐标属性和鼠标坐标属性
屏幕坐标属性:
TPM_CENTERALIGN 横向将菜单以x居中
TPM_LEFTALIGN 横向将菜单以x左对齐
TPM_RIGHTALIGN 横向将菜单以x右对齐
鼠标按键属性(只在响应WM_CONTEXTMENU消息时有效):
TPM_LEFTBUTTON 连续按? 右键不会连续弹出菜单,鼠标右键不可用于选定菜单项
TPM_RIGHTBUTTON 连续按鼠标右键会连续弹出菜单,鼠标右键可用于选定菜单项
x,y均为屏幕坐标
lpRect 菜单所占的区域。假如为NULL,当用户在菜单以外的区域按鼠标键时,菜单会消失


三、具体实现方法
用ClassWizard中的“Add Windows Message Handler”功能添加对NM_RCLICKT消息的响应函数,函数中代码如下,注释很周详,在vc6+win2000环境下调试成功,大家感兴趣的话自己试试吧:
void CMutiTalkingServerDlg::OnRClickListSessions(NMHDR* pNMHDR, LRESULT* pResult)
{
    // TODO: Add your control notification handler code here
    NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
    if(pNMListView->iItem != -1)
{
      DWORD dwPos = GetMessagePos();
      CPoint point( LOWORD(dwPos), HIWORD(dwPos) );
  
      CMenu menu;
      VERIFY( menu.LoadMenu( IDR_POPUPMENU ) );
      CMenu* popup = menu.GetSubMenu(0);
      ASSERT( popup != NULL );
      popup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
   }

   *pResult = 0;
}

 

 

 

 

在使用CListCtrl时要为它添加一个右键菜单,步骤如下:

1. 响应CListCtrlNM_RCLICK消息。

2. 添加一个菜单资源,在菜单资源中插入要添加到菜单内容。

可编辑子项的CListCtrl类_第1张图片

 

 

3.在列表控件右击响应函数中添加代码

void CListCtrlDlg::OnRclickList1(NMHDR* pNMHDR, LRESULT* pResult)

{

CPoint point;

::GetCursorPos(&point);

CMenu menu;

VERIFY(menu.LoadMenu(IDR_MENU1));      //IDR_MENU_POPUP是新建菜单ID

CMenu* popup=menu.GetSubMenu(0);

ASSERT(popup!=NULL );

popup->TrackPopupMenu(TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, this );

*pResult = 0;

}

显示效果如下:

可编辑子项的CListCtrl类_第2张图片

 

很简单,几步就搞定。

 

TrackPopupMenu函数的使用:(翻译自MSDN

BOOLTrackPopupMenu(UINTnFlags,intx,inty,CWnd*pWnd,LPCRECTlpRect=NULL);

返回值

如果函数成功返回非零,否则为0

 

参数nFlags

指定屏幕位置标志和鼠标按钮标志。屏幕位置标志可以是下列之一:

TPM_CENTERALIGN:若设置此标志,函数将按参数x指定的坐标水平居中放置弹出式菜单。

TPM_LEFTALIGN:若设置此标志,函数使弹出式菜单的左边界与由参数X指定的坐标对齐。

TPM_RIGHTALIGN:若设置此标志,函数使弹出式菜单的右边界与由参数X指定的坐标对齐。

鼠标按钮标志可以是下列之一:

TPM_LEFTBUTTON表示鼠标左键跟踪弹出菜单。

TPM_RIGHTBUTTON表示鼠标右键跟踪弹出菜单。

X

指定弹出式菜单在屏幕上的水平位置坐标。根据该nFlags参数值,菜单可以是左对齐,右对齐,或相对于该中心的位置。

Ÿ

指定菜单的顶部在屏幕上的垂直位置坐标。

pWnd

标识拥有弹出式菜单的窗口。此窗口接收来自菜单的所有WM_COMMAND消息。在Windows版本3.1和更高版本,直到TrackPopupMenu返回,窗口才接收WM_COMMAND消息。在Windows 3.0TrackPopupMenu返回之前窗口接收WM_COMMAND消息。

lpRect

指向一个RECT结构或CRect对象,Rect包含一个矩形的屏幕坐标,用户在这个矩形区域内点击鼠标,弹出式菜单不消息。如果这个参数为NULL,用户点击弹出式菜单以外的区域,菜单消失。Windows 3.0里面,这必须是空的。

对于Windows 3.1和更高版本,您可以使用下列常量

TPM_CENTERALIGN

TPM_LEFTALIGN

TPM_RIGHTALIGN

TPM_RIGHTBUTTON

备注:

在指定位置显示一个浮动的弹出式菜单并跟踪弹出式菜单项的选择。一个浮动的弹出菜单可以显示在屏幕上的任何地方。

 

 

在使用CListCtrl控件时,有时需要添加右键菜单来实现快捷操作,一般的步骤如下:

1. 在资源视图里添加一个菜单资源IDR_LIST_POPUPMENU,添加菜单项目,如下图所示

2. 响应CListCtrl的右键单击消息NM_RCLICK;

3. 在右键单击响应函数里添加以下代码,

void C××::OnNMRClickLstParatable(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
POINT pt;

GetCursorPos(&pt);

CMenu menu;
VERIFY(menu.LoadMenu(IDR_LIST_POPUPMENU));

CMenu* pPopup = menu.GetSubMenu(0); // 这个就是菜单在菜单资源条中的位置
ASSERT(pPopup != NULL);

// 控制菜单状态,单击空白处使菜单项变灰无效,当然也可以不显示菜单,看你的需要了
if(pNMItemActivate->iItem != -1)
        pPopup->EnableMenuItem(ID_DELETE, MF_ENABLED);
else
        pPopup->EnableMenuItem(ID_DELETE, MF_GRAYED);

pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,pt.x,pt.y,this);

*pResult = 0;
}

效果如下:

CListCtrl添加右键菜单 - 紫影 - 黎明前的听雨轩

CListCtrl添加右键菜单 - 紫影 - 黎明前的听雨轩

4. 最后就是添加右键菜单的响应函数就OK了。

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(可编辑子项的CListCtrl类)