Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Editable ListBox Tutorial

4.93/5 (34 votes)
24 May 200612 min read 1   7.9K  
Step-by-step development of an editable ListBox.

CEditableListBox demo application

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.

Image 2

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.

Image 3

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.

Image 4

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();

    .......
    .......

    // TODO: Add extra initialization here
    CString cs;

    for( int i = 1; i <= 200; i++ )
    {
        cs.Format( _T("Item %03d"), i );

        m_LB_Editable.AddString( cs );
    }

    // return TRUE  unless you set the focus to a control
    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:

// Operations
protected:
    void    EditStarts  ();
    void    EditEnds    ( BOOL bCommitText = TRUE );

In the implementation file for CEditableListBox, we write:

void CEditableListBox::EditStarts()
{
}

void CEditableListBox::EditEnds( BOOL bCommitText /* = TRUE */ )
{
}

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();

    // Anything selected?
    if( iItemCurrentlySelected != LB_ERR )
    {
        CRect rItemCurrentlySelected;

        GetItemRect( iItemCurrentlySelected, 
                     rItemCurrentlySelected );

        // If the user clicked on a previously
        // selected item, start the editing
        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.

// Attributes
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();

    // Anything selected?
    if( iSel != LB_ERR )
    {
        CRect rItem;

        // Get the rectangle and position
        // this item occupies in the listbox
        GetItemRect( iSel, rItem );

        // Make the rectangle a bit larger
        // top-to-bottom. Not necessary but looks better to me
        rItem.InflateRect( 0, 2, 0, 2 );

        // Create the edit control
        m_ceEdit.Create( WS_VISIBLE | WS_CHILD | 
           WS_BORDER | ES_LEFT | ES_AUTOHSCROLL, 
           rItem, this, 1 );

        // Give it the same font as the listbox
        m_ceEdit.SetFont( this->GetFont() );

        // Now add the item's text to it
        // and selected for convenience
        CString csItem;

        GetText( iSel, csItem );

        m_ceEdit.SetWindowText( csItem );
        m_ceEdit.SetSel( 0, -1, TRUE );

        // Set the focus on it
        m_ceEdit.SetFocus();

        // Record the item position
        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()
{
    // Is there an edit window lurking about? If so, get rid of it
    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 /* = TRUE */ )
{
    // Is there an edit window lurking
    // about? If so, we are in business
    if( m_ceEdit.GetSafeHwnd() != NULL )
    {
        // Do we want the text entered by the user
        // to replace the selected lb item's text?
        if( bCommitText && m_iItemBeingEdited != -1 )
        {
            CString csOldItem, csNewItem;

            GetText( m_iBeingEdited, csOldItem );
            m_ceEdit.GetWindowText( csNewItem );

            // If the new text is the same as the old, why bother
            if( csOldItem.Compare( csNewItem ) != 0 )
            {
                // Get rid of the lb item that we are replacing
                DeleteString( m_iItemBeingEdited );

                // If the listbox has the "sort" attribute on,
                // we add the new text and let it sort it out
                // Otherwise, we insert the new text
                // precisely where the old one was
                if( GetStyle() & LBS_SORT )
                    m_iItemBeingEdited = AddString( csNewItem );
                else
                    m_iItemBeingEdited = 
                      InsertString( m_iBeingEdited, csNewItem );

                // Select the lb item
                SetCurSel( m_iItemBeingEdited );
            }
        }

        // The editing is done, nothing is marked for editing
        m_iItemBeingEdited = -1;

        // Get rid of the editing window
        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 /* = TRUE */ )
{
    // Is there an edit window lurking about? If so, we are in business
    if( m_ceEdit.GetSafeHwnd() != NULL )
    {
        // Do we want the text entered by the user
        // to replace the selected lb item's text?
        if( bCommitText && m_iItemBeingEdited != -1 )
        {
            CString csOldItem, csNewItem;

            GetText( m_iBeingEdited, csOldItem );
            m_ceEdit.GetWindowText( csNewItem );

            // If the new text is the same as the old, why bother
            if( csOldItem.Compare( csNewItem ) != 0 )
            {
                ........
                ........

                // Let the parent know if LBS_NOTIFY is flagged
                if( GetStyle() & LBS_NOTIFY )
                    GetParent()->SendMessage( WM_APP_LB_ITEM_EDITED,
                                              (WPARAM)m_iItemBeingEdited,
                                              (LPARAM)(LPCTSTR)csOldItem );
            }
        }

        // The editing is done, nothing is marked for editing
        m_iItemBeingEdited = -1;

        // Get rid of the editing window
        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:

// Generated message map functions
//{{AFX_MSG(CCEditableListBox_DemoDlg)

........
........

//}}AFX_MSG
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)
    //{{AFX_MSG_MAP(CCEditableListBox_DemoDlg)

    ........
    ........

    //}}AFX_MSG_MAP
    ON_MESSAGE(WM_APP_LB_ITEM_EDITED, OnListBoxItemEdited)
END_MESSAGE_MAP()

........
........

LRESULT CCEditableListBox_DemoDlg::OnListBoxItemEdited(WPARAM wParam, LPARAM lParam)
{
    CString cs, csNewItem;

    // Index number of edited item
    cs.Format( "%d", (int)wParam );
    m_ST_IndexNumber.SetWindowText( cs );

    // Item's text before editing
    m_ST_OldText.SetWindowText( (LPCTSTR)lParam );

    // Item's text after editing == current item's text
    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()
{
    // TODO: Add extra validation here

    //    CResizableDialog::OnOK();
}

void CCEditableListBox_DemoDlg::OnCancel()
{
    // TODO: Add extra cleanup here

    //    CResizableDialog::OnCancel();
}

void CCEditableListBox_DemoDlg::OnClose()
{
    // TODO: Add your message handler
    // code here and/or call default

    CResizableDialog::OnOK();

    //    CResizableDialog::OnClose();
}

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 );   // Commit changes
    break;

    case VK_ESCAPE:
        GetParent()->SendMessage( WM_APP_ED_EDIT_FINISHED, 
                    (WPARAM)FALSE );  // Disregard changes
    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 /*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 );   // Commit changes

    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here