Editable MFC List Controls

A run-through on how to create a standard MFC List Control CListCtrl editable.

Visual Studio 2010 project downloadable from here.

Much of the credit for this must go to Zafir Anjum on the Codeguru site for an article called “Editable subitems” from which this post borrows quite heavily.

Step 1: Create the MFC Project

Start by creating a dialog-based application in Visual Studio. Select File > New > Project > MFC Application:

editableList1

When the Application Wizard is launched, select Dialog based as the Application Type:

editableList2

And then select Finish. In the Resources View, notice that a new dialog is created. Click on the image below in order to get a close-up:

editableList3

To get started, first delete the static control “TODO: Place dialog controls here” that gets automatically created here:

editableList4

In the Toolbox, select the “List Control” and in the Resource View place this within your dialog area:

editableList5

Right-click on the List Control you just added and select Properties. In the Properties window, make sure the View section of Appearance is set to ‘Report’ style:

editableList6

So that the appearance of the List Control as shown in the Resource View changes to this:

editableList7

Step 2: Derive a class from CListCtrl

Create a new List Control class CEditableListCtrl, that publicly inherits from the standard CListCtrl:

class CEditableListCtrl : public CListCtrl
{
public:
	int GetRowFromPoint( CPoint &point, int *col ) const;
	CEdit* EditSubLabel( int nItem, int nCol );

	void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
	void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);

	void OnEndLabelEdit(NMHDR* pNMHDR, LRESULT* pResult);
	void OnLButtonDown(UINT nFlags, CPoint point);
};

One important modification is to define a function GetRowFromPoint to determine the row and column number that the cursor falls on, if any:

int CEditableListCtrl::GetRowFromPoint( CPoint &point, int *col ) const
{
	int column = 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 rectangle of item and check whether point falls in it.
		CRect rect;		
		GetItemRect( row, &rect, LVIR_BOUNDS );		
		
		if( rect.PtInRect(point) )		
		{			
			// Find the column		
			for( column = 0; column < nColumnCount; column++ )			
			{				
				int colwidth = GetColumnWidth( column );	

				if( point.x >= rect.left && point.x <= (rect.left + colwidth ) )
				{					
					if( col ) *col = column;					
					return row;				
				}				
				
				rect.left += colwidth;
			}		
		}	
	}	
	
	return -1;

}

In this class we also add a method to edit the individual cells of the List Control. Taking the row and column integers as arguments, EditSubLabel creates and makes visible an edit control of the appropriate size and text justification. (The edit control class CInPlaceEdit is derived from the standard CEdit class and is described in the next section.)

CEdit* CEditableListCtrl::EditSubLabel( int nItem, int nCol )
{	
	// The returned pointer should not be saved, make sure item visible
	if( !EnsureVisible( nItem, TRUE ) ) return NULL; 	
	
	// Make sure that column number 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 );
	
	// Scroll horizontally 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_LIST1 );  
	
	return pEdit;
}

Another essential modification is to add the means for the user to initiate edit of the selected List Control cell by modify the OnLButtonDown method:

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

	ModifyStyle(0, LVS_EDITLABELS);
	
	int colnum;	
	
	if( ( index = GetRowFromPoint( point, &colnum ) ) != -1 )	
	{		
		UINT flag = LVIS_FOCUSED;
		
		if( (GetItemState( index, flag ) & flag) == flag /*&& colnum == 2*/ )		
		{			
			// 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 3: Derive a class from CEdit
We create a derived instance of the CEdit class in order to satisfy a number of requirements: It needs to send the LVN_ENDLABELEDIT message and self-destruct upon completion of editing; expand a little in order to accommodate the text; terminate upon pressing the Escape or Enter keys or when the edit control loses focus:

class CInPlaceEdit : public CEdit 
{

public:	
	CInPlaceEdit(int iItem, int iSubItem, CString sInitText); 
		
	// ClassWizard generated virtual function overrides	
	//{{AFX_VIRTUAL(CInPlaceEdit)	
	public:	virtual BOOL PreTranslateMessage(MSG* pMsg);	
	//}}AFX_VIRTUAL
		
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;
};

The modified CEdit control is described as follows.

The CInPlaceEdit constructor simply saves the values passed through its arguments and initializes m_bESC to false.

The overridden PreTranslateMessage() determines whether certain key strokes 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 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.

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

OnChar() function terminates editing 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 list view control to determine if there is space for the edit control to expand.

OnCreate() function creates the edit control, initialising it with the proper values.

Full listing of the modified CEdit control:

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

// Translate window messages before they are dispatched to the TranslateMessage and DispatchMessage Windows functions.
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);
} 
	
// Called immediately before losing the input focus
void CInPlaceEdit::OnKillFocus(CWnd* pNewWnd)
{	
	CEdit::OnKillFocus(pNewWnd); 
	CString str;	
	GetWindowText(str); 				

	DestroyWindow();
}

// Called when nonclient area is being destroyed
void CInPlaceEdit::OnNcDestroy()
{	
	CEdit::OnNcDestroy();
	delete this;
}  

// Called for nonsystem character keystrokes
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 
	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;			   

	// Get the client rectangle	
	CRect rect, parentrect;	
	GetClientRect( &rect );
	GetParent()->GetClientRect( &parentrect ); 
	
	// Transform rectangle to parent coordinates	
	ClientToScreen( &rect );	
	GetParent()->ScreenToClient( &rect ); 	
	
	// Check whether control needs resizing and if sufficient 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 );	
	}

	// Construct list control item data
	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(); 	

	// Send this Notification to parent of ListView ctrl	
	CWnd* pWndViewAttachmentsDlg = GetParent()->GetParent();

	if ( pWndViewAttachmentsDlg )
	{
		pWndViewAttachmentsDlg->SendMessage( WM_NOTIFY_DESCRIPTION_EDITED, 
			                                 GetParent()->GetDlgCtrlID(), 
											 (LPARAM)&dispinfo );
	}
}

// Called when application requests the Windows window be created by calling the Create/CreateEx member function.
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;
}

Step 4: Add the CEditableListCtrl as a control variable

In the main CEditableListControlDlg class, add CEditableListCtrl as a control variable:

CEditableListCtrl m_EditableList;

And modify DoDataExchange accordingly:

void CEditableListControlDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);

	DDX_Control(pDX, IDC_LIST1, m_EditableList);
}

And in OnInitDialog I add a few sample list control entries:

BOOL CEditableListControlDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// Add "About..." menu item to system menu.

	// IDM_ABOUTBOX must be in the system command range.
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		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);
		}
	}

	// Set the icon for this dialog.  The framework does this automatically
	//  when the application's main window is not a dialog
	SetIcon(m_hIcon, TRUE);			// Set big icon
	SetIcon(m_hIcon, FALSE);		// Set small icon

	// TODO: Add extra initialization here
	LVCOLUMN lvColumn;
	int nCol;

	lvColumn.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
	lvColumn.fmt = LVCFMT_LEFT;
	lvColumn.cx = 150;
	lvColumn.pszText = "Name";
	nCol = m_EditableList.InsertColumn(0, &lvColumn);
	
	lvColumn.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
	lvColumn.fmt = LVCFMT_CENTER;
	lvColumn.cx = 150;
	lvColumn.pszText = "Occupation";
	m_EditableList.InsertColumn(1, &lvColumn);

	lvColumn.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH;
	lvColumn.fmt = LVCFMT_LEFT;
	lvColumn.cx = 150;
	lvColumn.pszText = "Country";
	m_EditableList.InsertColumn(2, &lvColumn);

	m_EditableList.SetExtendedStyle(m_EditableList.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EDITLABELS);
	
	// Insert a few example list items
	int l_iItem = m_EditableList.InsertItem(LVIF_TEXT|LVIF_STATE, 0, "Andrew", 0, LVIS_SELECTED, 0, 0);		
	m_EditableList.SetItemText( l_iItem, 1, "Bricklayer" );
	m_EditableList.SetItemText( l_iItem, 2, "Australia" );

	l_iItem = m_EditableList.InsertItem(LVIF_TEXT|LVIF_STATE, 0, "Peter", 0, LVIS_SELECTED, 0, 0);		
	m_EditableList.SetItemText( l_iItem, 1, "Lecturer" );
	m_EditableList.SetItemText( l_iItem, 2, "New Zealand" );

	l_iItem = m_EditableList.InsertItem(LVIF_TEXT|LVIF_STATE, 0, "Richard", 0, LVIS_SELECTED, 0, 0);		
	m_EditableList.SetItemText( l_iItem, 1, "Dentist" );
	m_EditableList.SetItemText( l_iItem, 2, "Botswana" );

	return TRUE;  // return TRUE  unless you set the focus to a control
}

Step 5: Introduce notifiers for handling updates and Windows messages

Beginning with the Windows message map:

BEGIN_MESSAGE_MAP(CEditableListControlDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_NOTIFY(NM_CLICK, IDC_LIST1, OnNMClickList)
	ON_MESSAGE(WM_NOTIFY_DESCRIPTION_EDITED, OnNotifyDescriptionEdited)
END_MESSAGE_MAP()

In particular, for when the use clicks on the editable list control:

void CEditableListControlDlg::OnNMClickList(NMHDR *pNMHDR, LRESULT *pResult)
{
	m_fClickedList = true;
	m_EditableList.OnLButtonDown( MK_LBUTTON, InterviewListCursorPosition() );

	*pResult = 0;
}

I also add a notifier for handling every instance when the edit control has been updated:

LRESULT CEditableListControlDlg::OnNotifyDescriptionEdited(WPARAM wParam, LPARAM lParam)
{
	// Get the changed Description field text via the callback
	LV_DISPINFO* dispinfo = reinterpret_cast<LV_DISPINFO*>(lParam);	

	// Persist the selected attachment details upon updating its text
	m_EditableList.SetItemText( dispinfo->item.iItem, dispinfo->item.iSubItem, dispinfo->item.pszText );

	return 0;
}

Running the sample project the default List Control entries are displayed:

editableList10

And then see how upon left-clicking individual cells, they become editable, an outline appearing around the editable control:

editableList11

And that any of the fields, can be edited, if the user chooses:

editableList12

Download the Visual Studio 2010 project from here.

As always comments, feedback and suggestions are always welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>