Click here to Skip to main content
Click here to Skip to main content

Selection Highlighting of an Entire Row

, 26 Sep 2001
Rate this:
Please Sign up or sign in to vote.
How to select an entire row in a list view control.

Sample Image - selectentirerow.gif

Table of Contents

Preface

This article is a close adaption of the article "Selection Highlighting of Entire Row" at Codeguru. I've enhanced as much as I can while trying to keep as much original content as possible. I also included a summary of the article's comments.

Introduction

When the list view control is in the report view, only the first column of the selected row is highlighted. To enable highlighting of the entire row, the list view control has to be owner drawn. That is, the onus of highlighting the row is on the programmer. Visual C++ already comes with an MFC sample Rowlist that shows how to do this. The code given below was written after studying the sample code and fixes a few bugs present in the sample. One of the bug was that displaying state images messed up the text justification. Another bug was that if you resized the column to zero width (to hide it), the hidden columns content spilled over to the next column.

Note: Starting from Common Controls Library Version 4.70, you can also use the extended List-View style LVS_EX_FULLROWSELECT to acchieve the same result.

Step-By-Step

The code in the following steps uses MFC and its classes CListCtrl/CListView.

Step 1: Create the control with LVS_OWNERDRAWFIXED style

You can set the owner draw style from the resource editor if you have the list view control in a dialog resource. If you aren't already using a sub-class of CListCtrl then create one. You can use Class Wizard to add a member variable for the list view control.

If you are using a CListView derived class then add the LVS_OWNERDRAWFIXED fixed style in the PreCreateWindow() function. Here's an example

BOOL CListViewEx::PreCreateWindow(CREATESTRUCT& cs)
{
    // default is report view and full row selection
    cs.style &= ~LVS_TYPEMASK;
    cs.style &= ~LVS_SHOWSELALWAYS;
    cs.style |= LVS_REPORT | LVS_OWNERDRAWFIXED;
    cs.style |= LVS_EDITLABELS;

    return(CListView::PreCreateWindow(cs));
}

Step2: Add member variable and initialize it

Add a protected member variable m_nHighlight to your class declaration. Also add an enumeration for the different kinds of selection highlighting. The normal highlighting is when only the label in the first column is highlighted. When you choose HIGHLIGHT_ALLCOLUMNS, only the defined columns are highlighted. That is, any area to right of the last column. HIGHLIGHT_ROW on the other hand, covers the entire width of the client area.

Initialize m_nHighlight in the constructor to a value you feel should be the default. Since we are talking about full row selection it will probably be either HIGHLIGHT_ALLCOLUMNS or HIGHLIGHT_ROW.

public:
    enum EHighlight { HIGHLIGHT_NORMAL, HIGHLIGHT_ALLCOLUMNS, HIGHLIGHT_ROW };
protected:
    int m_nHighlight; // Indicate type of selection highlighting

Step 3: Override DrawItem()

The CListCtrl::DrawItem() function is called by the framework whenever an item needs to be drawn. Of course, this function is called only for owner drawn controls. The code for DrawItem() is somewhat long. Following is a brief description of what the code does:

  • After setting up some variables, the function first saves the device context sent in through the argument. We will use this to restore the device context when we are done. It then gets the image and the state flags for the item that needs to be drawn.
  • The next segment of code determines if the item needs to be highlighted. The condition for highlighting an item is that the item should be drop-highlighted or it should be selected. By default the selection is shown only if the control has the focus or if the control has the LVS_SHOWSELALWAYS style. You may have noticed with non owner drawn controls that if an item is highlighted when the control does not have focus then the highlight color is different (usually gray). In our code below we do not make this differentiation but you can easily add it if you want.
  • The next significant step is the statement where we set a variable to twice the width of a space character. This value is used as an offset when we draw the text in the columns. This offset is used to give a white space margin on both ends of the main label and the subitem text.
  • The function then computes the highlight rectangle and sets up the device context and draws the highlight. We then draw the state image, the item image and the item label (e.i. text for column 1). Before we actually draw any of these we set the clip region for the device context so that all output is constrained within the first column. For item images, you'll notice that we use ILD_BLEND50 flag for selected items. This gives the effect that the image is also selected. If you don't want the image to look different for selected items then remove this flag. Also note that the item label is always drawn left justified. This is the default behaviour of the list view control so we mimic that.
  • The DrawText() function is used to draw the text for all the columns. The last argument to this function is a flag that tells the function how to draw the text. One flag of special interest to us is the DT_END_ELLIPSIS. This causes the DrawText() function to replace part of the given string with ellipses, if necessary, so that the result fits in the column bounds. This is again meant to mimic the default behaviour of the control.
  • We then restore the text color and text background color if the highlighting was for the first column only and then we draw the remainder of the column texts. For the sub-item text we make sure that it gets the text justification information from the column and uses it. We finally draw the focus rectangle and restore the device context.
void CMyListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    CRect rcItem(lpDrawItemStruct->rcItem);
    int nItem = lpDrawItemStruct->itemID;
    CImageList* pImageList;
    
    // Save dc state
    int nSavedDC = pDC->SaveDC();
    
    // Get item image and state info
    LV_ITEM lvi;
    lvi.mask = LVIF_IMAGE | LVIF_STATE;
    lvi.iItem = nItem;
    lvi.iSubItem = 0;
    lvi.stateMask = 0xFFFF;     // get all state flags
    GetItem(&lvi);
    
    // Should the item be highlighted
    BOOL bHighlight =((lvi.state & LVIS_DROPHILITED)
        || ( (lvi.state & LVIS_SELECTED)
        && ((GetFocus() == this)
        || (GetStyle() & LVS_SHOWSELALWAYS)
        )
        )
        );
    
    // Get rectangles for drawing
    CRect rcBounds, rcLabel, rcIcon;
    GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
    GetItemRect(nItem, rcLabel, LVIR_LABEL);
    GetItemRect(nItem, rcIcon, LVIR_ICON);
    CRect rcCol( rcBounds ); 
    
    CString sLabel = GetItemText( nItem, 0 );
    
    // Labels are offset by a certain amount  
    // This offset is related to the width of a space character
    int offset = pDC->GetTextExtent(_T(" "), 1 ).cx*2;
    
    CRect rcHighlight;
    CRect rcWnd;
    int nExt;
    switch( m_nHighlight )
    {
    case 0: 
        nExt = pDC->GetOutputTextExtent(sLabel).cx + offset;
        rcHighlight = rcLabel;
        if( rcLabel.left + nExt < rcLabel.right )
            rcHighlight.right = rcLabel.left + nExt;
        break;
    case 1:
        rcHighlight = rcBounds;
        rcHighlight.left = rcLabel.left;
        break;
    case 2:
        GetClientRect(&rcWnd);
        rcHighlight = rcBounds;
        rcHighlight.left = rcLabel.left;
        rcHighlight.right = rcWnd.right;
        break;
    default:
        rcHighlight = rcLabel;
    }
    
    // Draw the background color
    if( bHighlight )
    {
        pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
        pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
        
        pDC->FillRect(rcHighlight, &CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
    }
    else
        pDC->FillRect(rcHighlight, &CBrush(::GetSysColor(COLOR_WINDOW)));
    
    // Set clip region
    rcCol.right = rcCol.left + GetColumnWidth(0);
    CRgn rgn;
    rgn.CreateRectRgnIndirect(&rcCol);
    pDC->SelectClipRgn(&rgn);
    rgn.DeleteObject();
    
    // Draw state icon
    if (lvi.state & LVIS_STATEIMAGEMASK)
    {
        int nImage = ((lvi.state & LVIS_STATEIMAGEMASK)>>12) - 1;
        pImageList = GetImageList(LVSIL_STATE);
        if (pImageList)
        {
            pImageList->Draw(pDC, nImage,
                CPoint(rcCol.left, rcCol.top), ILD_TRANSPARENT);
        }
    }
    
    // Draw normal and overlay icon
    pImageList = GetImageList(LVSIL_SMALL);
    if (pImageList)
    {
        UINT nOvlImageMask=lvi.state & LVIS_OVERLAYMASK;
        pImageList->Draw(pDC, lvi.iImage, 
            CPoint(rcIcon.left, rcIcon.top),
            (bHighlight?ILD_BLEND50:0) | ILD_TRANSPARENT | nOvlImageMask );
    }
    
    // Draw item label - Column 0
    rcLabel.left += offset/2;
    rcLabel.right -= offset;
    
    pDC->DrawText(sLabel,-1,rcLabel,
        DT_LEFT | DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP |
        DT_VCENTER | DT_END_ELLIPSIS);
    
    // Draw labels for remaining columns
    LV_COLUMN lvc;
    lvc.mask = LVCF_FMT | LVCF_WIDTH;
    
    if( m_nHighlight == 0 )     // Highlight only first column
    {
        pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
        pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));
    }
    
    rcBounds.right = rcHighlight.right > rcBounds.right ? rcHighlight.right :
    rcBounds.right;
    rgn.CreateRectRgnIndirect(&rcBounds);
    pDC->SelectClipRgn(&rgn);
    
    for(int nColumn = 1; GetColumn(nColumn, &lvc); nColumn++)
    {
        rcCol.left = rcCol.right;
        rcCol.right += lvc.cx;
        
        // Draw the background if needed
        if( m_nHighlight == HIGHLIGHT_NORMAL )
            pDC->FillRect(rcCol, &CBrush(::GetSysColor(COLOR_WINDOW)));
        
        sLabel = GetItemText(nItem, nColumn);
        if (sLabel.GetLength() == 0)
            continue;
        
        // Get the text justification
        UINT nJustify = DT_LEFT;
        switch(lvc.fmt & LVCFMT_JUSTIFYMASK)
        {
        case LVCFMT_RIGHT:
            nJustify = DT_RIGHT;
            break;
        case LVCFMT_CENTER:
            nJustify = DT_CENTER;
            break;
        default:
            break;
        }
        
        rcLabel = rcCol;
        rcLabel.left += offset;
        rcLabel.right -= offset;
        
        pDC->DrawText(sLabel, -1, rcLabel, nJustify | DT_SINGLELINE | 
            DT_NOPREFIX | DT_VCENTER | DT_END_ELLIPSIS);
    }
    
    // Draw focus rectangle if item has focus
    if (lvi.state & LVIS_FOCUSED && (GetFocus() == this))
        pDC->DrawFocusRect(rcHighlight);
    
    // Restore dc
    pDC->RestoreDC( nSavedDC );
}

Step 4: Define RepaintSelectedItems() helper function

This helper function is used to add or remove the focus rectangle around an item. It also adds or removes highlighting from a selected item depending on whether the control has the LVS_SHOWSELALWAYS style. Actually this function just invalidates the rectangle and DrawItem() takes care of the rest.

void CMyListCtrl::RepaintSelectedItems()
{
    CRect rcBounds, rcLabel;
    
    // Invalidate focused item so it can repaint 
    int nItem = GetNextItem(-1, LVNI_FOCUSED);
    
    if(nItem != -1)
    {
        GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
        GetItemRect(nItem, rcLabel, LVIR_LABEL);
        rcBounds.left = rcLabel.left;
        
        InvalidateRect(rcBounds, FALSE);
    }
    
    // Invalidate selected items depending on LVS_SHOWSELALWAYS
    if(!(GetStyle() & LVS_SHOWSELALWAYS))
    {
        for(nItem = GetNextItem(-1, LVNI_SELECTED);
        nItem != -1; nItem = GetNextItem(nItem, LVNI_SELECTED))
        {
            GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
            GetItemRect(nItem, rcLabel, LVIR_LABEL);
            rcBounds.left = rcLabel.left;
            
            InvalidateRect(rcBounds, FALSE);
        }
    }
    
    UpdateWindow();
}

Step 5: Add code to OnPaint() to invalidate entire row

When the list view control repaints an item, it repaints only the area occupied by defined columns. I.e. if the last column does not extend to the end of the client area, then the space to the right of the last column is not repainted. If we are highlighting the full row then this area also needs to be invalidated, so that the code in DrawItem() can add or remove the highlighting from this area.

We are handling the WM_PAINT message with OnPaint().

void CMyListCtrl::OnPaint() 
{
    // in full row select mode, we need to extend the clipping region
    // so we can paint a selection all the way to the right
    if (m_nHighlight == HIGHLIGHT_ROW &&
        (GetStyle() & LVS_TYPEMASK) == LVS_REPORT )
    {
        CRect rcBounds;
        GetItemRect(0, rcBounds, LVIR_BOUNDS);
        
        CRect rcClient;
        GetClientRect(&rcClient);
        if(rcBounds.right < rcClient.right)
        {
            CPaintDC dc(this);
            
            CRect rcClip;
            dc.GetClipBox(rcClip);
            
            rcClip.left = min(rcBounds.right-1, rcClip.left);
            rcClip.right = rcClient.right;
            
            InvalidateRect(rcClip, FALSE);
        }
    }
}

Step 6: Add handlers for WM_KILLFOCUS and WM_SETFOCUS

This is another step to mimic the default behaviour of the list view control. When the control loses focus, the focus rectangle around the selected (focus) item has to be removed. When the control gets back focus, then the focus rectangle has to be redrawn. Both these handlers call the RepaintSelectedItems() helper function.

We are handling the WM_KILLFOCUS message with OnKillFocus() and the WM_SETFOCUS message with OnSetFocus().

void CMyListCtrl::OnKillFocus(CWnd* pNewWnd) 
{
    CListCtrl::OnKillFocus(pNewWnd);
    
    // check if we are losing focus to label edit box
    if(pNewWnd != NULL && pNewWnd->GetParent() == this)
        return;
    
    // repaint items that should change appearance
    if((GetStyle() & LVS_TYPEMASK) == LVS_REPORT)
        RepaintSelectedItems();
}
void CMyListCtrl::OnSetFocus(CWnd* pOldWnd) 
{
    CListCtrl::OnSetFocus(pOldWnd);
    
    // check if we are getting focus from label edit box
    if(pOldWnd!=NULL && pOldWnd->GetParent()==this)
        return;
    
    // repaint items that should change appearance
    if((GetStyle() & LVS_TYPEMASK)==LVS_REPORT)
        RepaintSelectedItems();
}

Step 7: Add helper function to change selection mode

In this function we update the member variable m_nHighlight and invalidate the control so that the items are redrawn with the proper highlighting. SetHighlightType() returns the previous highlight value.

int CMyListCtrl::SetHighlightType(EHighlight hilite)
{
    int oldhilite = m_nHighlight;
    if( hilite <= HIGHLIGHT_ROW )
    {
        m_nHighlight = hilite;
        Invalidate();
    }
    return oldhilite;
}

Conclusion

So far the steps, adapted from the original article. Following is a summary of the most interesting comments at the original article. The comments include fixes and corrections to the previous steps. Since I want to leave the article as it is, I decided to not include suggested fixes in the code fragments above. It's up to you to use or not to use them. Another reason is, that I don't know whether all suggested fixes are correct. Please drop a comment here so that I can remove/adjust any errors.

Readers Comments

Horizontal Scrolling redraws incorrect

Problem:When scrolling the horizontal scroll bar, the part of the control that has been scrolled is not updated. The OnPaint and DrawItem are both called when scrolling in this way, but they do not seem to update the display. (James Siddle, 1998-10-30)

Solution:it appears that the example code draws the highlight a fraction too small for the width of the window. I haven't gone into the code in depth (and I don't intend to), but the following simple change will fix the problem. Find this section of code in DrawItem() and make the change that has been commented.

switch( m_nHighlight )
{
case 0: 
    nExt = pDC->GetOutputTextExtent(sLabel).cx + offset;
    rcHighlight = rcLabel;
    if( rcLabel.left + nExt < rcLabel.right )
    rcHighlight.right = rcLabel.left + nExt;
    break;
case 1:
    rcHighlight = rcBounds;
    rcHighlight.left = rcLabel.left;
    break;
case 2:
    GetClientRect(&rcWnd);
    rcHighlight = rcBounds;
    rcHighlight.left = rcLabel.left;
    rcHighlight.right = rcWnd.right+1;   // Add 1 to prevent trails
    break;
default:
    rcHighlight = rcLabel;
}

(James Siddle, 1998-10-30)

DrawItem not being called

Problem:DrawItem() not being called, but OnPaint() and InvalidateRect() are being called. (Mike Kuitunen, 1998-11-03)

Solution:DrawItem() is only called if the list view style is "report view". Do the following steps:

  1. Set the list view style to "report view".
  2. Check the owner draw fixed in the "More Styles" tab.
  3. Override the DrawItem() virtual function in a derived class.
  4. Insert some columns and items to the control.

(Mahmoud Saleh, 1999-06-10)

Solution 2:The DrawItem() function needs to be declared as follows:

void CMyListView::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{ 
}

The message map in the .cpp file should look like the following:

BEGIN_MESSAGE_MAP(CMyListView, CListView) 
    //{{AFX_MSG_MAP(CMyListView) 
    ON_WM_DRAWITEM() 
    //}}AFX_MSG_MAP 
END_MESSAGE_MAP()

And in the header file the Overrides part should look like this:

    // Overrides 
    // ClassWizard generated virtual function overrides 
    //{{AFX_VIRTUAL(CMyListView) 
public: 
    virtual void OnInitialUpdate(); 
protected: 
    virtual void OnDraw(CDC* pDC); // overridden to draw this view 
    virtual void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint); 
    virtual BOOL PreCreateWindow(CREATESTRUCT& cs); 
    virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); 
    //}}AFX_VIRTUAL 

(Peter Hendrix, 1998-11-04)

DrawItem() is not thread safe

Problem/Solution:
I implemented this code myself, and also added text string dependant coloring in DrawItem(). From that point my Visual C++ 5.0 application would get a 60 byte memory block overrun in a totally different destructor, in DEBUG mode only.

I now upgraded to 6.0 and now this memory error shows in Release, so now I had to solve it. I found the problem to be that DrawItem() is not thread safe, my application uses several threads, each with it's own CMyListCtrl object. When thread locking DrawItem(), the problem goes away.

Here's how to make DrawItem() thread safe:

void CMyListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    CMutex mutex(FALSE, "MyListCtrl::DrawItem()");
    CSingleLock lock(&mutex);
    lock.Lock(); // Wait until signalled. 
                 // Unlock takes place in destructor when function exit

    CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    CRect rcItem(lpDrawItemStruct->rcItem);

    ...
}

(Peter Sjöström, 1999-02-17)

Color text in List Control

Problem/Solution:
The following code is color text of list control:

if( bHighlight ) 
{ 
    pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
    pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
    pDC->FillRect(rcHighlight, &CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
} 
else
{
    pDC->FillRect(rcHighlight, &CBrush(nItem%2 ?::GetSysColor(COLOR_WINDOW) 
        : RGB(255,255,0))); 
    pDC->SetTextColor( RGB(255,0,255) );
}

(Kelvin Liu, 1999-08-24)

Assertion when inserting Items

Problem:
I read the article carefully and I think I made all the suggested changes, e.g. creating a derived class from CListCtrl, named CMyList, subclass the control with a member variable from CMyList, copied all the code to the derived class, change the default style of the control to owner-drawn fixed, but when I try to insert some items I get an assertion failure. (Unknown, 2000-02-23)

Solution:

// full row select 
DWORD dwStyle = ::SendMessage([listctrl hwnd], 
LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0); 

dwStyle |= LVS_EX_FULLROWSELECT; 

::SendMessage([listctrl hwnd], 
LVM_SETEXTENDEDLISTVIEWSTYLE, 0, dwStyle);

(Tom Archer, 2000-03-18)

Solution2:
Instead of OnDrawItem(), override DrawItem(), which asserts by default. (Denis, 2000-03-27)

OnDraw() Not being called

Problem/Solution:
MFC does not seem to call OnDraw(), if OnPaint() has been overridden. Once I removed my override for OnPaint() (WM_PAINT), OnDraw() started getting called. (Mark Hetherington, 2000-10-18)

See also/References

  1. Selection Highlighting of Entire Row, Zafir Anjum, 1998-08-06, www.codeguru.com.
  2. Changing Row Height in an owner drawn Control, Uwe Keim, 2001-09-27, www.codeproject.com
  3. Neat Stuff to do in List Controls Using Custom Draw, Michael Dunn, 1999-11-30, www.codeproject.com.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Uwe Keim
Chief Technology Officer Zeta Producer Desktop CMS
Germany Germany
Uwe does programming since 1989 with experiences in Assembler, C++, MFC and lots of web- and database stuff and now uses ASP.NET and C# extensively, too. He has also teached programming to students at the local university.
 
In his free time, he does climbing, running and mountain biking. Recently he became a father of a cute boy.
 
Some cool, free software from us:
 
Free Test Management Software - Intuitive, competitive, Test Plans. Download now!  
Homepage erstellen - Intuitive, very easy to use. Download now!  
Send large Files online for free by Email
Offline-Homepage-Baukasten

Comments and Discussions

 
QuestionListview selecting the whole row Pinmemberocean_blue428-Dec-11 1:24 
Generalopen file problem Pinmemberashtwin27-Apr-05 22:40 
GeneralMultiselect in a List control Pinmemberreshmasp8-Mar-04 0:12 
Generalmuch more simpler way to achieve the same PinmemberDuvadesa4-Jul-02 0:35 
GeneralRe: much more simpler way to achieve the same PinsussAnonymous1-Nov-03 0:34 
GeneralRe: much more simpler way to achieve the same Pinmembercjpm10031-Mar-04 2:25 
GeneralRe: much more simpler way to achieve the same PinsitebuilderUwe Keim31-Mar-04 3:00 
GeneralRe: much more simpler way to achieve the same Pinmembercjpm10031-Mar-04 3:08 
GeneralRe: much more simpler way to achieve the same Pinmember_18720-Apr-04 2:45 
GeneralCode/Improvments by "Ross" PinsitebuilderUwe Keim27-May-02 6:05 
GeneralRe: Code/Improvments by &quot;Ross&quot; Pinmembermctainsh211-Jun-03 15:24 
QuestionWhy ? PinmemberAnonymous25-Sep-01 22:34 
AnswerRe: Why ? PinmemberUwe Keim25-Sep-01 22:58 
AnswerLike so... Pinmemberjustncase8026-Jun-03 20:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 27 Sep 2001
Article Copyright 2001 by Uwe Keim
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid