A run-through on how to create a editable MFC List Control by making the 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:
When the Application Wizard is launched, select Dialog based as the Application Type:
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:
To get started, first delete the static control “TODO: Place dialog controls here” that gets automatically created here:
In the Toolbox, select the “List Control” and in the Resource View place this within your dialog area:
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:
So that the appearance of the List Control as shown in the Resource View changes to this:
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:
And then see how upon left-clicking individual cells, they become editable, an outline appearing around the editable control:
And that any of the fields, can be edited, if the user chooses:
Download the Visual Studio 2010 project from here.
As always comments, feedback and suggestions are always welcome.