Editable subitems


The default implementation of the ListView control allows editing of the first column label only. You have to create your own edit control to allow editing of subitems.

Step 1: Derive a class from CListCtrl

In the code below, CMyListCtrl is the name used for the derived class. You can also derive a class from CListView if you need this functionality in a CView rather than in a control. If you are already working with a sub-class of CListCtrl, you can make the modifications to that class.

Step 2: Define HitTestEx()

Define an extended HitTest function for the CMyListCtrl class. This function will determine the row index that the point falls over and also determine the column. The HitTestEx() has already been listed and explained in an earlier section and is listed here again for completeness. We need this function if the user interface to initiate the edit is a mouse click or a double click. See the section "Detecting column index of the item clicked".
// HitTestEx	- Determine the row index and column index for a point
// Returns	- the row index or -1 if point is not over a row
// point	- point to be tested.
// col		- to hold the column index
int CMyListCtrl::HitTestEx(CPoint &point, int *col) const
{
	int colnum = 0;
	int row = HitTest( point, NULL );
	
	if( col ) *col = 0;

	// Make sure that the ListView is in LVS_REPORT
	if( (GetWindowLong(m_hWnd, GWL_STYLE) & LVS_TYPEMASK) != LVS_REPORT )
		return row;

	// Get the top and bottom row visible
	row = GetTopIndex();
	int bottom = row + GetCountPerPage();
	if( bottom > GetItemCount() )
		bottom = GetItemCount();
	
	// Get the number of columns
	CHeaderCtrl* pHeader = (CHeaderCtrl*)GetDlgItem(0);
	int nColumnCount = pHeader->GetItemCount();

	// Loop through the visible rows
	for( ;row <=bottom;row++)
	{
		// Get bounding rect of item and check whether point falls in it.
		CRect rect;
		GetItemRect( row, &rect, LVIR_BOUNDS );
		if( rect.PtInRect(point) )
		{
			// Now find the column
			for( colnum = 0; colnum < nColumnCount; colnum++ )
			{
				int colwidth = GetColumnWidth(colnum);
				if( point.x >= rect.left 
					&& point.x <= (rect.left + colwidth ) )
				{
					if( col ) *col = colnum;
					return row;
				}
				rect.left += colwidth;
			}
		}
	}
	return -1;
}

Step 3: Add function to initiate the edit

The user interface to initiate an edit for a sub item may be click on an already selected row, a double click or even a push button. We define a helper function to set up the edit control. The helper function takes only the row and column index of the subitem. EditSubLabel() ensures that the row as well as the column is visible before it creates the edit control. It then creates the edit control of the right size and with the proper text justification. The edit control created is of the class CInPlaceEdit which we will define later.
// EditSubLabel		- Start edit of a sub item label
// Returns		- Temporary pointer to the new edit control
// nItem		- The row index of the item to edit
// nCol			- The column of the sub item.
CEdit* CMyListCtrl::EditSubLabel( int nItem, int nCol )
{
	// The returned pointer should not be saved

	// Make sure that the item is visible
	if( !EnsureVisible( nItem, TRUE ) ) return NULL;

	// Make sure that nCol is valid
	CHeaderCtrl* pHeader = (CHeaderCtrl*)GetDlgItem(0);
	int nColumnCount = pHeader->GetItemCount();
	if( nCol >= nColumnCount || GetColumnWidth(nCol) < 5 )
		return NULL;

	// Get the column offset
	int offset = 0;
	for( int i = 0; i < nCol; i++ )
		offset += GetColumnWidth( i );

	CRect rect;
	GetItemRect( nItem, &rect, LVIR_BOUNDS );

	// Now scroll if we need to expose the column
	CRect rcClient;
	GetClientRect( &rcClient );
	if( offset + rect.left < 0 || offset + rect.left > rcClient.right )
	{
		CSize size;
		size.cx = offset + rect.left;
		size.cy = 0;
		Scroll( size );
		rect.left -= size.cx;
	}

	// Get Column alignment
	LV_COLUMN lvcol;
	lvcol.mask = LVCF_FMT;
	GetColumn( nCol, &lvcol );
	DWORD dwStyle ;
	if((lvcol.fmt&LVCFMT_JUSTIFYMASK) == LVCFMT_LEFT)
		dwStyle = ES_LEFT;
	else if((lvcol.fmt&LVCFMT_JUSTIFYMASK) == LVCFMT_RIGHT)
		dwStyle = ES_RIGHT;
	else dwStyle = ES_CENTER;

	rect.left += offset+4;
	rect.right = rect.left + GetColumnWidth( nCol ) - 3 ;
	if( rect.right > rcClient.right) rect.right = rcClient.right;

	dwStyle |= WS_BORDER|WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL;
	CEdit *pEdit = new CInPlaceEdit(nItem, nCol, GetItemText( nItem, nCol ));
	pEdit->Create( dwStyle, rect, this, IDC_IPEDIT );


	return pEdit;
}

Step 4: Handle the scroll messages

The CInPlaceEdit class is designed to destroy the edit control and delete the object when it loses focus. Clicking on the scrollbars of the ListView control does not take away the focus from the edit control. We therefore add handlers for the scrollbar messages which force focus away from the edit control by setting the focus to the list view control itself.
void CMyListCtrl::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
	if( GetFocus() != this ) SetFocus();
	CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
}

void CMyListCtrl::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
	if( GetFocus() != this ) SetFocus();
	CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar);
}

Step 5: Handle EndLabelEdit

Like the built in edit control for the first column, our edit control also sends the LVN_ENDLABELEDIT notification when the edit is completed. If this notification message isnt already being handled, add a handler so that any changes made with the edit control can be accepted.
void CMyListCtrl::OnEndLabelEdit(NMHDR* pNMHDR, LRESULT* pResult)
{
	LV_DISPINFO *plvDispInfo = (LV_DISPINFO *)pNMHDR;
	LV_ITEM	*plvItem = &plvDispInfo->item;

	if (plvItem->pszText != NULL)
	{
		SetItemText(plvItem->iItem, plvItem->iSubItem, plvItem->pszText);
	}
	*pResult = FALSE;
}

Step 6: Add means for the user to initiate the edit

A suggested method for starting an edit of a subitem is to click on a subitem when the item already has the focus. You may choose to provide a different method. The code below is the handler for the WM_LBUTTONDOWN message. An edit control is created to edit a subitem if the subitem is clicked after the item already has the focus. The code checks for the LVS_EDITLABELS style before creating the edit control. It also does not activate the edit control for the first column since editing of the first column is already supported by the list view control.

void CMyListCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
	int index;
	CListCtrl::OnLButtonDown(nFlags, point);

	int colnum;
	if( ( index = HitTestEx( point, &colnum )) != -1 )
	{
		UINT flag = LVIS_FOCUSED;
		if( (GetItemState( index, flag ) & flag) == flag && colnum > 0)
		{
			// Add check for LVS_EDITLABELS
			if( GetWindowLong(m_hWnd, GWL_STYLE) & LVS_EDITLABELS )
				EditSubLabel( index, colnum );
		}
		else
			SetItemState( index, LVIS_SELECTED | LVIS_FOCUSED ,
				    	LVIS_SELECTED | LVIS_FOCUSED); 
	}
}

Step 7: Subclass the CEdit class

We need to subclass the CEdit class to provide for our special requirement. The main requirements placed on this class is that
  • It should send the LVN_ENDLABELEDIT message when edit is complete
  • It should expand to accommodate the text
  • It should destroy itself when the edit is complete
  • The edit should be terminated when the user presses the Escape or the Enter key or when the edit control loses focus.
The listing of the header file precedes that of the implementation file. The CInPlaceEdit declares four private variables. These are used when the control sends the LVN_ENDLABELEDIT notification.
// InPlaceEdit.h : header file
//

/////////////////////////////////////////////////////////////////////////////
// CInPlaceEdit window

class CInPlaceEdit : public CEdit
{
// Construction
public:
	CInPlaceEdit(int iItem, int iSubItem, CString sInitText);

// Attributes
public:

// Operations
public:

// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CInPlaceEdit)
	public:
	virtual BOOL PreTranslateMessage(MSG* pMsg);
	//}}AFX_VIRTUAL

// Implementation
public:
	virtual ~CInPlaceEdit();

	// Generated message map functions
protected:
	//{{AFX_MSG(CInPlaceEdit)
	afx_msg void OnKillFocus(CWnd* pNewWnd);
	afx_msg void OnNcDestroy();
	afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
	afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
	//}}AFX_MSG

	DECLARE_MESSAGE_MAP()
private:
	int m_iItem;
	int m_iSubItem;
	CString m_sInitText;
	BOOL    m_bESC;	 	// To indicate whether ESC key was pressed
};

/////////////////////////////////////////////////////////////////////////////
The listing of the implementation file now follows. The CInPlaceEdit constructor simply saves the values passed through its arguments and initializes m_bESC to false. The overridden PreTranslateMessage() is to ascertain that certain key strokes do make it to the edit control. The escape key and the enter key are normally pre-translated by the CDialog or the CFormView object, we therefore specifically check for these and pass it on to the edit control. The check for GetKeyState( VK_CONTROL) makes sure that key combinations such as Ctrl-C, Ctrl-V and Ctrl-X get forwarded to the edit control.

OnKillFocus() sends of the LVN_ENDLABELEDIT notification and destroys the edit control. The notification is sent to the parent of the list view control and not to the list view control itself. When sending the notification, the m_bESC member variable is used to determine whether to send a NULL string.

The OnNcDestroy() function is the appropriate place to destroy the C++ object.

The OnChar() function ends the edit if the Escape or the Enter key is pressed. It does this by setting focus to the list view control which force the OnKillFocus() of the edit control to be called. For any other character, the OnChar() function lets the base class function take care of it before it tries to determine if the control needs to be resized. The function first gets the extent of the new string using the proper font and then compares it to the current dimension of the edit control. If the string is too long to fit within the edit control, it resizes the edit control after checking the parent window ( the list view control ) to determine if there is space for the edit control to grow.

The OnCreate() function creates the edit control and initializes it with the proper values.

// InPlaceEdit.cpp : implementation file
//

#include "stdafx.h"
#include "InPlaceEdit.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CInPlaceEdit

CInPlaceEdit::CInPlaceEdit(int iItem, int iSubItem, CString sInitText)
:m_sInitText( sInitText )
{
	m_iItem = iItem;
	m_iSubItem = iSubItem;
	m_bESC = FALSE;
}

CInPlaceEdit::~CInPlaceEdit()
{
}


BEGIN_MESSAGE_MAP(CInPlaceEdit, CEdit)
	//{{AFX_MSG_MAP(CInPlaceEdit)
	ON_WM_KILLFOCUS()
	ON_WM_NCDESTROY()
	ON_WM_CHAR()
	ON_WM_CREATE()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CInPlaceEdit message handlers

BOOL CInPlaceEdit::PreTranslateMessage(MSG* pMsg)
{
	if( pMsg->message == WM_KEYDOWN )
	{
		if(pMsg->wParam == VK_RETURN
				|| pMsg->wParam == VK_DELETE
				|| pMsg->wParam == VK_ESCAPE
				|| GetKeyState( VK_CONTROL)
				)
		{
			::TranslateMessage(pMsg);
			::DispatchMessage(pMsg);
			return TRUE;		    	// DO NOT process further
		}
	}

	return CEdit::PreTranslateMessage(pMsg);
}


void CInPlaceEdit::OnKillFocus(CWnd* pNewWnd)
{
	CEdit::OnKillFocus(pNewWnd);

	CString str;
	GetWindowText(str);

	// Send Notification to parent of ListView ctrl
	LV_DISPINFO dispinfo;
	dispinfo.hdr.hwndFrom = GetParent()->m_hWnd;
	dispinfo.hdr.idFrom = GetDlgCtrlID();
	dispinfo.hdr.code = LVN_ENDLABELEDIT;

	dispinfo.item.mask = LVIF_TEXT;
	dispinfo.item.iItem = m_iItem;
	dispinfo.item.iSubItem = m_iSubItem;
	dispinfo.item.pszText = m_bESC ? NULL : LPTSTR((LPCTSTR)str);
	dispinfo.item.cchTextMax = str.GetLength();

	GetParent()->GetParent()->SendMessage( WM_NOTIFY, GetParent()->GetDlgCtrlID(), 
					(LPARAM)&dispinfo );

	DestroyWindow();
}

void CInPlaceEdit::OnNcDestroy()
{
	CEdit::OnNcDestroy();

	delete this;
}


void CInPlaceEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
	if( nChar == VK_ESCAPE || nChar == VK_RETURN)
	{
		if( nChar == VK_ESCAPE )
			m_bESC = TRUE;
		GetParent()->SetFocus();
		return;
	}


	CEdit::OnChar(nChar, nRepCnt, nFlags);

	// Resize edit control if needed

	// Get text extent
	CString str;

	GetWindowText( str );
	CWindowDC dc(this);
	CFont *pFont = GetParent()->GetFont();
	CFont *pFontDC = dc.SelectObject( pFont );
	CSize size = dc.GetTextExtent( str );
	dc.SelectObject( pFontDC );
	size.cx += 5;			   	// add some extra buffer

	// Get client rect
	CRect rect, parentrect;
	GetClientRect( &rect );
	GetParent()->GetClientRect( &parentrect );

	// Transform rect to parent coordinates
	ClientToScreen( &rect );
	GetParent()->ScreenToClient( &rect );

	// Check whether control needs to be resized
	// and whether there is space to grow
	if( size.cx > rect.Width() )
	{
		if( size.cx + rect.left < parentrect.right )
			rect.right = rect.left + size.cx;
		else
			rect.right = parentrect.right;
		MoveWindow( &rect );
	}
}

int CInPlaceEdit::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
	if (CEdit::OnCreate(lpCreateStruct) == -1)
		return -1;

	// Set the proper font
	CFont* font = GetParent()->GetFont();
	SetFont(font);

	SetWindowText( m_sInitText );
	SetFocus();
	SetSel( 0, -1 );
	return 0;
}



Comments

  • En anden vigtig ting er at et ghd glattejern ikke bliver sÃ¥ varmt som andre glattejern

    Posted by wanzixiao on 05/30/2013 10:22am

    [url=http://www.ghd.webgarden.com/]Billige ghd Glattejern[/url] ghd glattejern manke frisør ædle metal Sørg for ideen reelt kan være ukompliceret at du bare utvivlsomt kunne anskaffe din egen hele hovedet af hår direkte, den faktiske går tilbage, med ikke søger at erhverve en person Lad du mange hunner forudsætning hairsalon hver periode de foretrækker måske at have direkte hår. Forudsat, hvor de faktisk ikke definitivt modtager vilde hår lige uden hjælp dybest set især har problemer med sammen med regionen reelle trunk spot inde i låsene. Mange punkter, som kan være almindeligt tilgængelige inde i en stor mængde af scenarier har tendens til at være ofte betydelige generelt betydeligt. Dit eksisterende forbrug kan meget let faa meget trætte inde situation du gøre arbejdet, der har kommer tilbage igen inden for hår inde i denne særlig typografi, oprette en Prøv nøjagtige fremragende løsninger. [url=http://www.ghdfladjerntilbud.webstarts.com/]ghd fladjern tilbud[/url] Hår er en kvindes identitet, Enhver kvinde håber alle, at gennem de forskellige stilarter til at ændre billedet, Face, at gøre sig smukkere, mere ungdommeligt, eller elegance, vil man være i stand til at opfylde alles idé om hår, Vi lærer sammen, hvordan til at vælge billige ghd glattejern? ghd glattejern, hvordan du bruger? Forberedelse. [url=http://www.ghdglattejerntilbud.webs.com/]ghd glattejern tilbud[/url] Ved hjælp af magi magi ghd glattejern, kan du oprette en stor bølge af hår. Meget velegnet til off bryst hår pige. Brug er meget enkel, først ghd håret varme, hvis man kan operere mere anstrengende omvej first hår ghd håret på genopvarmning, men dette er ikke god temperaturregulering, først med en ghd fladjern hår for klemmer bor, og hår rundt om s-gerne på det, ville det tage at være stor, er du nødt til at cirkel rundt om et par mindre punkter, hvis du ønsker at tilbringe et lille sår på multi-point Best Buy keramik, færre skader ens hår!

    Reply
  • Scroll wheel?

    Posted by Leo on 02/06/2013 02:06am

    Thanks for the article, however I would have appreciated if complete code was availabale as a newbie MFC developer. Haven't checked all comments, but the code in its original form does not handle mouse scroll wheel actions. The same code as for OnVScroll and OnHScroll seems to work, except this handler must return a value. BOOL CMyListCtrl::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { if( GetFocus() != this ) SetFocus(); CListCtrl::OnMouseWheel(nFlags, zDelta, pt); return true; }

    Reply
  • Thanks about this article ! I appriciate !

    Posted by koheunhee on 10/13/2012 02:45am

    But after edit subitem memory leak occurred. these leaks same as number of editing subitem. maybe after create CEdit on heap , not deleted. I Tried delete the CEdit heap memory But I can't please help me and sorry about my poor english

    Reply
  • RE: GetKeyState( VK_CONTROL) always returns 1

    Posted by fotw on 07/26/2010 05:40am

    Of course it does. 
    Allright, it's not clearly stated in GetKeyState documentation, but if you look at GetKeyboardState you can read: "The low-order bit is meaningless for non-toggle keys." 
    
    It's again not clear but it gives me the clue to plain just discard it:
    
    SHORT iCtrlKeyState = GetKeyState( VK_CONTROL ) & 0xFF00; // or 0xFFFE if you preffer
    
    ...that's all

    Reply
  • Code Request

    Posted by youssefhg on 07/14/2009 02:03pm

    Hi Zafir I was wondering if you can post a demo project or the source code for this project as I am having a hard time embedding this code to my project that has a list which i need to edit its subitems. Thanks Youssef

    Reply
  • Please Help

    Posted by hliyanachchi on 07/16/2008 01:51am

    How to Declare the  IDC_IPEDIT ?

    • Resource.h

      Posted by Triplemeh on 06/24/2009 04:29pm

      declare it in Resource.h e.g. #define IDC_IPEDIT 420

      Reply
    Reply
  • Please help

    Posted by tze on 05/12/2006 10:23am

    What is IDC_IPEDIT ? How to declare it ?

    Reply
  • Great article

    Posted by vijaymohite on 08/26/2004 11:35am

    This is really a great article! Thanks for posting such a gr8 article! :) ~Vijay.

    Reply
  • good

    Posted by iamcrying on 03/11/2004 04:35am

    very good

    Reply
  • Problem w/ Right-Justified Text (ES_RIGHT)

    Posted by Legacy on 01/27/2004 12:00am

    Originally posted by: Zack

    If you have a column set as right-justified, the code correctly adjusts the edit style to ES_RIGHT so that the edit matches. However, when the edit is drawn, the top of the border is "cut off". I have experimented with this code in several instances and this always happens. If I change the size of the edit in the code (change the rect's top/bottom), it doesn't help until the edit is much larger than the "cell" size. Anyone have a solution?

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: September 10, 2014 Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild." This loop of continuous delivery and continuous feedback is how the best mobile …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds