List Control: Header Drag, Column Resize, and Remember User Settings

Published on
11,867 Points
3 Endorsements
Last Modified:
Here's how to let your user rearrange and resize the columns in your ListView Control, and have them be that same way the next time he runs your program.

The Common Controls ListView Control in "Report View" style is an excellent way to display a grid of data.  It has many capabilities, including the ability to let the user rearrange the columns and resize them.  That sort of customization is not always needed, but in a case where the user will spend a lot of time with the grid, you really don't want to lock him in to whatever you, the developer, think would be the best layout.
Drag headers to rearrange column orderBy default, the ListView control lets your user make columns wider and narrower, and it's not hard to turn on the feature that allows drag-and-drop column headers to rearrange the columns,  In the dialog editor, you will also need to set the View property to Report, and make sure that "No Column Header" is FALSE, then add this code:
    m_ctlList.SetExtendedStyle( LVS_EX_HEADERDRAGDROP );

Open in new window

...to the dialog initialization section.    

But after the user rearranges the columns, your program won't remember the column order settings the next time it runs unless you take action to save and restore them.  The place to save such settings is in the registry.  All you really need is the column order and the width of each column.
Save arrangement in the registryTo set up such a scheme, your program creates the list in the normal way, setting  LV_COLUMN.SubItemId to sequential values.  Then use the header control's SetItem function to specify HDITEM.iOrder and HDITEM.cxy with different sequences and widths, as saved in a previous session.  

This is all handled in the CListCtrlEx class object defined in the following source code:

CListCtrlEx header file
#pragma once
// CListCtrlEx.h

//--------------- these two structs are used to define the default column layout
typedef enum {
	SORT_None = 0,
	SORT_AZ   = 1,
	SORT_ZA   = -1,
} SortOrder;

typedef struct {
	int       nId;
	int       nClmNum;
	int       nWidth;       // pixels
	SortOrder eSortOrder;
	LPSTR     pszName;
} ListClmInfo;

#define CNUM_MaxListClms 20
//------------------------------------------- the class object extends CListCtrl
class CListCtrlEx : public CListCtrl

	virtual ~CListCtrlEx();

	void SetupClms( ListClmInfo* parLCI, BOOL fSetFromSaved=FALSE, LPCSTR szKey=0 );

	void SaveClmInfo(     LPCSTR sKey ); 
	void UseSavedClmInfo( LPCSTR sKey ); 

	void EraseList();
	BOOL PutItem(int nItem,int nSubItem, int nVal,      BOOL fAddRow=FALSE );
	BOOL PutItem(int nItem,int nSubItem, LPCTSTR sItem, BOOL fAddRow=FALSE );

	void SaveClmHdrInfo( LPCSTR sKey, int nClms, LPINT paiIndexes, LPINT paiWidths );
	BOOL ReadClmHdrInfo( LPCSTR sKey, int nClms, LPINT paiIndexes, LPINT paiWidths );


Open in new window

CListCtrlEx source file
// ListCtrlEx.cpp : implementation file
#include "stdafx.h"
#include "ListClms.h"
#include "ListCtrlEx.h"


//----------------------------------- nothing special for the ctor or dtor


// Save and restore user prefs on column positions and widths
// to the registry key for this program
void CListCtrlEx::SaveClmInfo( LPCSTR szKey ) 

	int nClmCnt= GetHeaderCtrl()->GetItemCount();

	int anClmIdxs[   CNUM_MaxListClms ];
	int anClmWidths[ CNUM_MaxListClms ];

	for (int j=0; j< nClmCnt; j++ ) {
		rHdr.mask= HDI_WIDTH | HDI_ORDER;
		GetHeaderCtrl()->GetItem(j, &rHdr);	
		anClmIdxs[   j ] = rHdr.iOrder;
		anClmWidths[ j ] = rHdr.cxy;
	SaveClmHdrInfo( szKey, nClmCnt, anClmIdxs, anClmWidths );

void CListCtrlEx::UseSavedClmInfo( LPCSTR szKey ) 

	int nClmCnt= GetHeaderCtrl()->GetItemCount();

	int anClmIdxs[   CNUM_MaxListClms ];
	int anClmWidths[ CNUM_MaxListClms ];

	if ( ReadClmHdrInfo( szKey, nClmCnt, anClmIdxs, anClmWidths ) ) {
		for (int j=0; j< nClmCnt; j++ ) {
			if ( anClmWidths[j] == 0 ) anClmWidths[j]= 5;

			rHdr.mask= HDI_WIDTH | HDI_ORDER;
			rHdr.iOrder= anClmIdxs[   j ];
			rHdr.cxy   = anClmWidths[ j ];
			GetHeaderCtrl()->SetItem(j, &rHdr);	
// private functions used by the above to get data from registry
void CListCtrlEx::SaveClmHdrInfo( LPCSTR szKey, int nClms, LPINT paiIndexes, LPINT paiWidths )
	CString sAll="";
	CString sOne;
	if (szKey==0) szKey= "ListClms";
	for (int j=0; j< nClms; j++ ) {
		sOne.Format("%d,", *paiIndexes++ );
		sAll += sOne;
		sOne.Format("%d, ", *paiWidths++ );
		sAll += sOne;
	AfxGetApp()->WriteProfileString("Prefs", sKey, sAll);
BOOL CListCtrlEx::ReadClmHdrInfo( LPCSTR szKey, int nClms, LPINT paiIndexes, LPINT paiWidths )
	if (szKey==0) szKey= "ListClms";

	CString sAll= AfxGetApp()->GetProfileString("Prefs", szKey );
	if (sAll.IsEmpty()) {
		return FALSE;
	sAll += "                                ";
	int nCurPos=0;
	for (int j=0; j<nClms; j++ ) {

		*paiIndexes++ = atoi( sAll.Mid( nCurPos ) );
		nCurPos= sAll.Find( ',', nCurPos ) +1; // get past the comma
		if ( nCurPos <= 0) return( FALSE );

		*paiWidths++  = atoi( sAll.Mid( nCurPos ) );
		nCurPos= sAll.Find( ',', nCurPos ) +1; // get past the comma
		if ( nCurPos <= 0) return( FALSE );
	return TRUE;

// Generic utilities simplify use of ListView control

// Set up the columns -- given title, order and widths from a setup array
void CListCtrlEx::SetupClms( ListClmInfo* parLCI, BOOL fSetFromSaved /*=FALSE*/, LPCSTR szKey /*=0*/ )

	EraseList();  // delete all columns

	while (parLCI->nId != -1 ) {
		rLVC.cx=       parLCI->nWidth;     // in pixels
		rLVC.iSubItem= parLCI->nId;        // 
		rLVC.pszText=  parLCI->pszName;
		rLVC.fmt=      LVCFMT_LEFT;

		int nRet= InsertColumn( parLCI->nClmNum, &rLVC );
	if ( fSetFromSaved ) {
		UseSavedClmInfo( szKey );

void CListCtrlEx::EraseList()
	while( DeleteColumn(0) )
// Used in populating the list -- overloads given for an int or a string value
// Use fAddRow=TRUE when populating the first column in the new row
BOOL CListCtrlEx::PutItem(int nItem,int nSubItem, int nVal, BOOL fAddRow /*=FALSE*/ )
	CString str;  str.Format("%d",nVal);
	return( PutItem(nItem, nSubItem, str) );

BOOL CListCtrlEx::PutItem(int nItem,int nSubItem, LPCTSTR sItem, BOOL fAddRow /*=FALSE*/)
	LV_ITEM         lvItm;
	lvItm.mask=     LVIF_TEXT;
	lvItm.iItem=    nItem;
	lvItm.iSubItem= nSubItem;
	lvItm.pszText=  (LPSTR)sItem;
	if( nSubItem == 0 && fAddRow ) {
		return InsertItem( &lvItm );
	return SetItem( &lvItm );

Open in new window

The CListCtrl object will save the column layout to the application-defined registry key, in a subkey named prefs.  You can save layouts for multiple lists -- a common need in a data-centric application program -- by passing a subkey name into the SaveClmInfo and UseSavedColmInfo member functions.

The object provides a clean and easy way to define the initial column layout.  You don't need to use it, but I think you'll find it handy.  Just create an array of ListClmInfo structures and pass it to CListCtrlEx.SetupClms().  For instance:
ListClmInfo arClmsList[]= {
  {  0,  0, 130, SORT_None, "Name"     },
  {  1,  1,  90, SORT_None, "Created"  },
  {  2,  2,  60, SORT_None, "Your Ref" },
  {  3,  3,  80, SORT_None, "Type"     },
  {  4,  4,  40, SORT_None, "Status"   },
  {  5,  5,  80, SORT_None, "Comment"  },
  { -1,  0,   0, SORT_None, 0          }
m_ctlList.SetExtendedStyle( LVS_EX_HEADERDRAGDROP );

m_ctlList.SetupClms( arClmsList, FALSE );

Open in new window

To use this, just use the dialog editor to create a ListView control object.  Set its properties to Report View.  Right-click it, choose Add Variable..., and create a control-type variable (e.g., m_ctlList).  Then in the dialog's header, change the line:
	//CListCtrl m_ctlList;  // put here by the class wizard
	CListCtrlEx m_ctlList;  // <<<==== Change to this

Open in new window

You can create and populate your lists in whatever way you normally do, and the save/restore functions will work as described.

A commonly-asked question here at EE is "How can I let the user change the sort order by clicking on a header?"  I've be covered that in another EE Article.  See:

      List Control: Sorting Columns on a Header Click

If you liked this article and want to see more from this author,  please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions

Featured Post

Free Tool: ZipGrep

ZipGrep is a utility that can list and search zip (.war, .ear, .jar, etc) archives for text patterns, without the need to extract the archive's contents.

One of a set of tools we're offering as a way to say thank you for being a part of the community.

Join & Write a Comment

This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month