The list view control does not provide any visual feedback on whether the list is sorted. To give feedback to the users of our application, we can use the owner draw feature of the header control and display a triangle pointing downwards or pointing updwards, indicating whether the list is sorted in the ascending or the descending order. This, of course, is applicable only if the list view control is in the report view mode.
Since we have to use an owner drawn header control, we will need to derive a class from CHeaderCtrl and add the functionality in this class. Here are the steps involved.
Step 1: Derive class from CHeaderCtrl
If you don’t have a class derived from CHeaderCtrl, derive one now. You can use the Class Wizard to create one for you. I used the name CMyHeaderCtrl for the derived class.
Step 2: Add member variables
Add member variables in CMyHeaderCtrl to track the column that is sorted and the sorting order. These variables are declared as protected members and we will provide a function to set them.
protected: int m_nSortCol; BOOL m_bSortAsc;
Step 3: Initialize the variables in the constructor
Initialize the m_nSortCol variable to -1 in the constructor. This indicates that the list is not sorted.
CMyHeaderCtrl:: CMyHeaderCtrl() { m_nSortCol = -1; }
Step 4: Add function SetSortImage()
Add a function SetSortImage() to the CMyHeaderCtrl class. This is the function that will be used to set the sort indicator. The SetSortImage() function takes the column number as an argument and also a boolean value to indicate whether it is sorted in the ascending order or in the descending order.
After setting the internal variables, the function set the header item to owner drawn. This will ensure that the DrawItem() function will get called. The function then invalidates the header control so that any previous sort indicator is removed and the new one is displayed.
int CMyHeaderCtrl::SetSortImage( int nCol, BOOL bAsc ) { int nPrevCol = m_nSortCol; m_nSortCol = nCol; m_bSortAsc = bAsc; // Change the item to ownder drawn HD_ITEM hditem; hditem.mask = HDI_FORMAT; GetItem( nCol, &hditem ); hditem.fmt |= HDF_OWNERDRAW; SetItem( nCol, &hditem ); // Invalidate header control so that it gets redrawn Invalidate(); return nPrevCol; }
Step 5: Override DrawItem()
The DrawItem() is where the sort indicator actually gets drawn. Besides drawing the sort triangle, this function is now also responsible for drawing the column label itself. The DrawItem() function is called for each item in the header control that has the HDF_OWNERDRAW format.
These are the step we take in the DrawItem() function to draw the column label and the triangular image to indicate the sort order:
- Attach the device context handle passed in through the argument to a CDC object for easier device context handling. The handle is detached from the CDC object before the function returns. If we did not detach the handle then the DC would be released when the CDC object is destroyed.
- We save the DC and change the clipping region so that all the updates are contrained within the header item for which the DrawItem() function is called. The device context is restored before the function returns.
- We compute the offset used when drawing the label and the sort triangle. The offset is used to leave a margin around the label and is equal to twice the width of a space character.
- We determine the format to be used when drawing the column label. Since the column label can be aligned left, center or right, we have to choose an appropriate format for the DrawText() function. You will also notice the flag DT_END_ELLIPSIS. This tells the DrawText() function that if the text doesn’t fit with the rectangle specified, then the text should be shortened and three dots appended to the text so that the result fits within the rectangle.
- We next adjust the rectangle within which the label will be drawn and then draw the lable using DrawText().
- Finally we draw the triangle to indicate the sort order. We use two different color to draw the triangle so that it matches the other GUI elements in Widnows. The COLOR_3DHILIGHT color is used for edges facing the light source, and the COLOR_3DSHADOW color is used for the shadow.
void CMyHeaderCtrl::DrawItem( LPDRAWITEMSTRUCT lpDrawItemStruct ) { CDC dc; dc.Attach( lpDrawItemStruct->hDC ); // Get the column rect CRect rcLabel( lpDrawItemStruct->rcItem ); // Save DC int nSavedDC = dc.SaveDC(); // Set clipping region to limit drawing within column CRgn rgn; rgn.CreateRectRgnIndirect( &rcLabel ); dc.SelectObject( &rgn ); rgn.DeleteObject(); // Draw the background dc.FillRect(rcLabel, &CBrush(::GetSysColor(COLOR_3DFACE))); // Labels are offset by a certain amount // This offset is related to the width of a space character int offset = dc.GetTextExtent(_T(" "), 1 ).cx*2; // Get the column text and format TCHAR buf[256]; HD_ITEM hditem; hditem.mask = HDI_TEXT | HDI_FORMAT; hditem.pszText = buf; hditem.cchTextMax = 255; GetItem( lpDrawItemStruct->itemID, &hditem ); // Determine format for drawing column label UINT uFormat = DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP | DT_VCENTER | DT_END_ELLIPSIS ; if( hditem.fmt & HDF_CENTER) uFormat |= DT_CENTER; else if( hditem.fmt & HDF_RIGHT) uFormat |= DT_RIGHT; else uFormat |= DT_LEFT; // Adjust the rect if the mouse button is pressed on it if( lpDrawItemStruct->itemState == ODS_SELECTED ) { rcLabel.left++; rcLabel.top += 2; rcLabel.right++; } // Adjust the rect further if Sort arrow is to be displayed if( lpDrawItemStruct->itemID == (UINT)m_nSortCol ) { rcLabel.right -= 3 * offset; } rcLabel.left += offset; rcLabel.right -= offset; // Draw column label if( rcLabel.left < rcLabel.right ) dc.DrawText(buf,-1,rcLabel, uFormat); // Draw the Sort arrow if( lpDrawItemStruct->itemID == (UINT)m_nSortCol ) { CRect rcIcon( lpDrawItemStruct->rcItem ); // Set up pens to use for drawing the triangle CPen penLight(PS_SOLID, 1, GetSysColor(COLOR_3DHILIGHT)); CPen penShadow(PS_SOLID, 1, GetSysColor(COLOR_3DSHADOW)); CPen *pOldPen = dc.SelectObject( &penLight ); if( m_bSortAsc ) { // Draw triangle pointing upwards dc.MoveTo( rcIcon.right - 2*offset, offset-1); dc.LineTo( rcIcon.right - 3*offset/2, rcIcon.bottom - offset ); dc.LineTo( rcIcon.right - 5*offset/2-2, rcIcon.bottom - offset ); dc.MoveTo( rcIcon.right - 5*offset/2-1, rcIcon.bottom - offset-1 ); dc.SelectObject( &penShadow ); dc.LineTo( rcIcon.right - 2*offset, offset-2); } else { // Draw triangle pointing downwords dc.MoveTo( rcIcon.right - 3*offset/2, offset-1); dc.LineTo( rcIcon.right - 2*offset-1, rcIcon.bottom - offset + 1 ); dc.MoveTo( rcIcon.right - 2*offset-1, rcIcon.bottom - offset ); dc.SelectObject( &penShadow ); dc.LineTo( rcIcon.right - 5*offset/2-1, offset -1 ); dc.LineTo( rcIcon.right - 3*offset/2, offset -1); } // Restore the pen dc.SelectObject( pOldPen ); } // Restore dc dc.RestoreDC( nSavedDC ); // Detach the dc before returning dc.Detach(); }
Step 6: Add member variable for header control in list view class
Now that we are done with the CMyHeaderCtrl class, we have to add a member to the CListCtrl or the CListView derived class so that we can access the extended functionality. Add a protected member.
protected: CMyHeaderCtrl m_headerctrl;
Step 7: Subclass the header control
We have to sub-class the header control so that the DrawItem() function in CMyHeaderCtrl can get called. If you are using a CListView derived class, you can place the sub-classing code in OnInitialUpdate(). If you are using a CListCtrl derived class, then put the code in PreSubclassWindow(). In either case, make sure you call the base class version of the function before subclassing the header control.
If the listview control was not created in the report view mode, then you have to change the style of control before trying the subclass the control. You can use ModifyStyle() for this. The reason why we need to change the style to the report view mode is that the header control is created only when the control first taken to the report view mode.
void CMyListCtrl::PreSubclassWindow() { CListCtrl::PreSubclassWindow(); // Add initialization code m_headerctrl.SubclassWindow( ::GetDlgItem(m_hWnd,0) ); }
Step 8: Use SetSortImage() to indicate sort order
Now you are all set to add the sort order indicator. Whenever you sort the list view control, call the CMyHeaderCtrl::SetSortImage() function with the column number on which the list is sorted and the order of sorting. E.g.
m_headerctrl.SetSortImage( nCol, bAscending );