Introduction
The intended audience for this tutorial is the rookie programmer. In fact, the project is so simple that it could be used as a first foray into extending the functionality of existing controls. Because I know how baffling initial encounters can be, I have documented nearly every line of the code, and have explained, in almost painful detail, each step one needs to take in order to assemble an editable listbox control.
In previous tutorials (for example, CStatic derived custom tree control), my goal has been to illustrate "how the basic functionality of apparently complex GUI controls can be recreated with relatively straightforward code", and I have cautioned that the controls offered were not meant to replace available MFC controls. This is not the case now. In this tutorial, we will develop a so-called drop-in replacement, that is, a control that can be used instead of the available MFC control.
If you don't care about the tutorial and just want the control, click on download sources only, include all the files in your project, and replace CListBox
for CEditableListBox
wherever you want an editable listbox. You may want to know that CEditableListBox
informs its parent after an item has been edited, if LBS_NOTIFY
is flagged. Look at the header file for details. Note also that, for simplicity's sake, the code only works with single-selection listboxes. The modification for multiple selection will be left as a reader exercise.
What is to be Accomplished
The target is a listbox whose items can be edited in place. Nothing more. This is a very simple project, but the result will be a control that could be used in real projects without any modification.
The keen reader may like to know that I have written this tutorial as I wrote the demo project. The instructions, explanations, and code below do amount to the development of the editable listbox control in the image above. Be advised, however, that the demo project contains the final code only, so that the truly fun and frivolous sashays we will delve into throughout the tutorial might not entirely exist but in our imagination.
On with the code.
Step-by-Step Procedure
Project Kick-off
The setup is simple. Create a new dialog-based project, and set the warning level to 4 (Project Settings, C/C++ tab). Level 4 will ensure that anything suspicious is brought up to our attention so that it is up to us to decide what to do with 'informational warnings which in most cases can be safely ignored' (from the docs).
Let's start working on the listbox control. Create a new MFC class named CEditableListBox
that uses CListBox
as the base class.
In the resource editor, add a listbox control with ID IDC_LB
. I chose the listbox to be Multi-column for no reason whatsoever, but this means, we also need to check the Horizontal scroll, or some items will be unreachable. Whether or not the Vertical scroll is checked doesn't matter when the listbox is Multi-column, but I prefer to uncheck it, again, for no reason whatsoever.
Using the MFC ClassWizard, add a member variable to IDC_LB
named m_LB_Editable
, making sure to select Control
as the Category and CEditableListBox
as the Variable Type.
On clicking on OK, a message box warns us to make sure we have included the header file for the class CEditableListBox
in our dialog code. Do it now if you haven't already.
Since our control is derived from CListBox
, all the functionality available to CListBox
is also available to CEditableListBox
. If you compile and run the project now, you will have a listbox that behaves like any other listbox. Make sure to add some test items to the listbox to have something to play with.
BOOL CEditableListBox_DemoDlg::OnInitDialog()
{
CDialog::OnInitDialog();
.......
.......
CString cs;
for( int i = 1; i <= 200; i++ )
{
cs.Format( _T("Item %03d"), i );
m_LB_Editable.AddString( cs );
}
return TRUE;
}
The Listbox Control
For the items in the listbox to be editable, there are two matters to address: first, we need to develop a way for the user to type in some text; second, we need to update the selected listbox item with whatever text the user has entered. So, let's add a couple of protected methods to our CEditableListBox
class, one to address the first issue, and the other to address the second. We will leave the declarations empty for now.
In the header file for CEditableListBox
, we write:
protected:
void EditStarts ();
void EditEnds ( BOOL bCommitText = TRUE );
In the implementation file for CEditableListBox
, we write:
void CEditableListBox::EditStarts()
{
}
void CEditableListBox::EditEnds( BOOL bCommitText )
{
}
Well, what do we want the user to do in order to trigger the editing of an item? Let's settle for a right-click on a previously selected item. So, let's start by adding message handlers for WM_RBUTTONUP
and WM_LBUTTONDOWN
. The first will start the editing if the user right-clicks on a previously selected item. The second will end editing if the user left-clicks anywhere else on the listbox. The code is quite simple:
void CEditableListBox::OnRButtonUp(UINT nFlags, CPoint point)
{
int iItemCurrentlySelected = GetCurSel();
if( iItemCurrentlySelected != LB_ERR )
{
CRect rItemCurrentlySelected;
GetItemRect( iItemCurrentlySelected,
rItemCurrentlySelected );
if( rItemCurrentlySelected.PtInRect( point ) )
EditStarts();
}
CListBox::OnRButtonUp(nFlags, point);
}
void CEditableListBox::OnLButtonDown(UINT nFlags, CPoint point)
{
EditEnds();
CListBox::OnLButtonDown(nFlags, point);
}
Now, the interesting part. What does it take to edit a listbox item? For one, we are going to need a CEdit
control. We will place it right on top of the item to be edited, giving the impression that it is actually part of the listbox control. First, we need to declare a protected CEdit
member in the header of CEditableListBox
. We will also keep a record of the index position of the item being edited.
protected:
CEdit m_ceEdit;
int m_iItemBeingEdited;
Now, we are ready for the implementation of EditStarts
. It looks as follows:
void CEditableListBox::EditStarts()
{
int iSel = GetCurSel();
if( iSel != LB_ERR )
{
CRect rItem;
GetItemRect( iSel, rItem );
rItem.InflateRect( 0, 2, 0, 2 );
m_ceEdit.Create( WS_VISIBLE | WS_CHILD |
WS_BORDER | ES_LEFT | ES_AUTOHSCROLL,
rItem, this, 1 );
m_ceEdit.SetFont( this->GetFont() );
CString csItem;
GetText( iSel, csItem );
m_ceEdit.SetWindowText( csItem );
m_ceEdit.SetSel( 0, -1, TRUE );
m_ceEdit.SetFocus();
m_iBeingEdited = iSel;
}
}
The edit control is created on the fly, sized and placed properly, given the listbox's font, initialized to the selected item's text, and set the focus on. The editing is now on its way, but we are not yet ready to go. We need to make sure that the window created with m_ceEdit.Create( ... )
is destroyed eventually. We will take care of it in the EditEnds
method but, just in case the application closes mid-editing, let's add code to CEditableListBox
's destructor.
CEditableListBox::~CEditableListBox()
{
if( m_ceEdit.GetSafeHwnd() != NULL )
m_ceEdit.DestroyWindow();
}
If we can get a handle to the editing window, it surely means that it is still hanging around. The same premise holds for the EditEnds
method.
void CEditableListBox::EditEnds( BOOL bCommitText )
{
if( m_ceEdit.GetSafeHwnd() != NULL )
{
if( bCommitText && m_iItemBeingEdited != -1 )
{
CString csOldItem, csNewItem;
GetText( m_iBeingEdited, csOldItem );
m_ceEdit.GetWindowText( csNewItem );
if( csOldItem.Compare( csNewItem ) != 0 )
{
DeleteString( m_iItemBeingEdited );
if( GetStyle() & LBS_SORT )
m_iItemBeingEdited = AddString( csNewItem );
else
m_iItemBeingEdited =
InsertString( m_iBeingEdited, csNewItem );
SetCurSel( m_iItemBeingEdited );
}
}
m_iItemBeingEdited = -1;
m_ceEdit.DestroyWindow();
}
}
Do not be surprised if I tell you that, in a way, we are finished. It is that simple. The listbox's items are now editable, and the listbox itself behaves adroitly. You can compile, run, and edit a few items. Note that since the listbox in the demo project has the LBS_SORT
flag, edited items may be rearranged so that the listbox contents remain in alphabetical order.
However, do not be surprised either, if I tell you that, in many ways, we are not finished. Ever. There are a few more things we can do to make our editable listbox more apt. For example, what happens if while editing, the user is tempted by the urge to scroll the listbox? Should we interrupt the editing and commit the changes when the scrollbars are being exercised? It sounds like a good idea.
Add message handlers for WM_HSCROLL
and WM_VSCROLL
, and include a single line, calling for an end to all editing. The method EditEnds
takes care of it all.
void CEditableListBox::OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
EditEnds();
CListBox::OnHScroll(nSBCode, nPos, pScrollBar);
}
void CEditableListBox::OnVScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
EditEnds();
CListBox::OnVScroll(nSBCode, nPos, pScrollBar);
}
What if the listbox is resized? Nothing could be simpler than adding code to end editing in this case too. Well? Don't waste a minute, my friend, fire up the ClassWizard and add a message handler for WM_SIZE
. Now guess, what will the implementation look like?
void CEditableListBox::OnSize(UINT nType, int cx, int cy)
{
CListBox::OnSize(nType, cx, cy);
EditEnds();
}
Nothing to it.
We can also add keyboard support without much effort. Fire up the ClassWizard, add a message handler for WM_KEYUP
, and modify the code as follows:
void CEditableListBox::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if( nChar == VK_INSERT )
EditStarts();
CListBox::OnKeyUp(nChar, nRepCnt, nFlags);
}
If the user presses the Insert key, the edit will be triggered. Note that we don't need to check if an item is currently selected or not, the EditStarts
method carries out all checks.
An Additional Listbox Message
The listbox contents are now editable but only the listbox knows this. If the user modifies the text of an item, how is the parent dialog to know? What we need is for CEditableListBox
to send a message to its parent, communicating these crucial happenings. That is, What, Where, and When. Observe that the How is of no concern to the parent dialog as this functionality (editing) is encapsulated inside the listbox together with all other things listboxiesque, and is irrelevant outside of it.
In order to create a message, a #define
is added to the header file of CEditableListBox
, thus:
#define WM_APP_LB_ITEM_EDITED ( WM_APP + 04100 )
And a SendMessage
is added to the EditEnds
code as follows:
void CEditableListBox::EditEnds( BOOL bCommitText )
{
if( m_ceEdit.GetSafeHwnd() != NULL )
{
if( bCommitText && m_iItemBeingEdited != -1 )
{
CString csOldItem, csNewItem;
GetText( m_iBeingEdited, csOldItem );
m_ceEdit.GetWindowText( csNewItem );
if( csOldItem.Compare( csNewItem ) != 0 )
{
........
........
if( GetStyle() & LBS_NOTIFY )
GetParent()->SendMessage( WM_APP_LB_ITEM_EDITED,
(WPARAM)m_iItemBeingEdited,
(LPARAM)(LPCTSTR)csOldItem );
}
}
m_iItemBeingEdited = -1;
m_ceEdit.DestroyWindow();
}
}
The parent dialog needs to set up a message handler to precisely handle the message when it comes. In the declaration file of the parent dialog, include the following:
........
........
afx_msg LRESULT OnListBoxItemEdited(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
In the implementation file of the parent dialog, include the following:
BEGIN_MESSAGE_MAP(CCEditableListBox_DemoDlg, CResizableDialog)
........
........
ON_MESSAGE(WM_APP_LB_ITEM_EDITED, OnListBoxItemEdited)
END_MESSAGE_MAP()
........
........
LRESULT CCEditableListBox_DemoDlg::OnListBoxItemEdited(WPARAM wParam, LPARAM lParam)
{
CString cs, csNewItem;
cs.Format( "%d", (int)wParam );
m_ST_IndexNumber.SetWindowText( cs );
m_ST_OldText.SetWindowText( (LPCTSTR)lParam );
m_LB_Editable.GetText( m_LB_Editable.GetCurSel(), csNewItem );
m_ST_NewText.SetWindowText( csNewItem );
return FALSE;
}
Naturally, the actual implementation of OnListBoxItemEdited
will depend on what is needed in your application. What is important is that the parent dialog will be notified when a listbox item has been edited. The wParam
parameter in the message will tell where the edited item is in the listbox. Finally, the lParam
parameter in the message will tell what has changed, that is, the old text of the listbox item.
A More Capable Edit
While it is true - again - that we are finished and that CEditableListBox
will work just fine, there are a couple of things that could be improved without much complication. For example, what if the user wants to cancel the edit and restore the original text? There is no way of doing that right now other than to re-type what was there. Not pretty, not convenient. Better to use the Escape key, for instance. Also, how about committing the changes by simply pressing the Enter key? If you press either key right now, the demo projects simply close.
Well, since the Escape and Enter keys are being used by the parent dialog, the first thing we need to do is to free these keys. Go to the resource editor, bring up the dialog, and double click on the "OK" and "Cancel" buttons. This will create message handlers for OnOK
and OnCancel
. Now, fire up the ClassWizard, and add to your parent dialog a message handler for WM_CLOSE
. Modify these methods as follows:
void CCEditableListBox_DemoDlg::OnOK()
{
}
void CCEditableListBox_DemoDlg::OnCancel()
{
}
void CCEditableListBox_DemoDlg::OnClose()
{
CResizableDialog::OnOK();
}
The dialog doesn't close anymore when the Escape and Enter keys are pressed. Of course, both "OK" and "Cancel" buttons are now rendered worthless, so we may as well delete them. If we want to close the dialog, we will need to click on the "x" in the titlebar.
Now, we need to create our own CEdit
derived control with just enough functionality to serve our purposes. In particular, we need our derived CEdit
to inform CEditableListBox
when either key is pressed. We know how to do this.
Create a new MFC class named CEditHelper
that uses CEdit
as the base class. Now, add a #define
message to the header file of CEditHelper
as we did before.
#define WM_APP_ED_EDIT_FINISHED ( WM_APP + 04101 )
How do we capture the Escape and Enter keys? We add a message handler for WM_KEYUP
, that's how. From there, we will send the message to CEditableListBox
with the WPARAM
parameter set to TRUE
if we want the entered text to be committed to the listbox, and a FALSE
if we want to disregard the entered text.
void CEditHelper::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
switch( nChar )
{
case VK_RETURN:
GetParent()->SendMessage( WM_APP_ED_EDIT_FINISHED,
(WPARAM)TRUE );
break;
case VK_ESCAPE:
GetParent()->SendMessage( WM_APP_ED_EDIT_FINISHED,
(WPARAM)FALSE );
break;
}
CEdit::OnKeyUp(nChar, nRepCnt, nFlags);
}
Now, we need to handle this new message in CEditableListBox
. First, include the header for CEditHelper
in the header for CEditableListBox
, and replace the class type of m_ceEdit
from CEdit
to CEditHelper
. Second, put in place the message handler for WM_APP_ED_EDIT_FINISHED
as we did before. Its implementation looks as follows:
LRESULT CEditableListBox::OnEditFinished(WPARAM wParam, LPARAM )
{
EditEnds( (BOOL)wParam );
return FALSE;
}
Pretty simple indeed.
There is just one thing I would like to do before calling it quits. You may have noticed that if you press the tab key while editing, the edit control just hangs there. The same thing happens if you click on another control. In other words, if the edit loses the focus, the application behaves unseemly.
It is by no means difficult to adequately resolve this travesty. Fire up the ClassWizard, and add a message handler for WM_KILLFOCUS
to our CEditHelper
. We will add code to commit and end the editing when the edit control loses the focus.
void CEditHelper::OnKillFocus(CWnd* pNewWnd)
{
GetParent()->SendMessage( WM_APP_ED_EDIT_FINISHED,
(WPARAM)TRUE );
CEdit::OnKillFocus(pNewWnd);
}
Because CEditHelper
is now keen on WM_KILLFOCUS
messages, we do not need to worry about the EditEnds
in CEditableListBox
's OnLButtonDown
. Thus, let's remove the message handler for WM_LBUTTONDOWN
. Nothing feels healthier than getting rid of redundant code.
Do observe, my friend, that the same does not hold true for the other handlers (WM_HSCROLL
, WM_VSCROLL
, and WM_SIZE
) that also end the editing. We need these. Play around with them, and discover by yourself the Why's.
Are We There Yet?
Quite so.
Take a moment to look back on the project. What does it take to make an editable listbox? Truly, no more than a couple of methods, one to start editing and one to end it. Everything else revolves around these two methods.
Yet, despite its terseness, the functionality of the standard listbox control has been extended to include a useful operation, namely, in-place editing of listbox items. Furthermore, this has been accomplished without compromising the existing functionality of the standard MFC CListBox
control. From now on, when you need this kind of a control, all you need to do is to simply replace an existing listbox class type, from CListBox
to CEditableListBox
, and you are ready to go. Thus, here we have a drop-in replacement.
Now What?
Well, as I mentioned in the introduction, the code only works with single-selection listboxes (or rather, it works for multiple-selection but not quite as it should). The modifications necessary for the code to work properly with multiple-selection listboxes are left as an exercise for those so disposed. It isn't complicated, but I thought that I would leave something for you to do.
You may also want to implement other ways to trigger the editing, for example, by double-clicking on the item of your fancy. Whatever. Tweak the code, and don't be afraid of making mistakes. An immense crowd made them all before you and, for safe measure, untold many will recreate them anew after you. Thus reads the creed. Our exploits amount to nothing more than a minuscule and scarcely perceptible corpuscle of gaffe amid a universe of reverberating blunders.
Feedback
My intention has been to provide a tutorial that is coded clearly, and as simple to understand and follow as possible. I am sure that there are finer solutions to the functionality I have implemented here. Any suggestions that improve, simplify, or better explain the code are welcome.
Acknowledgments
For the demo project, I've used an old version of CResizableDialog by Paolo Messina that I found lying around my HD. Great code, thanks Paolo.
Other than that, I want to acknowledge everyone at CodeProject. My appreciation goes to the folks that make it happen and, especially, to all those guys that continue to freely share what they know. Thank you all.