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

DanRollins
CERTIFIED EXPERT
Published:
Updated:
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
                      {
                      	DECLARE_DYNAMIC(CListCtrlEx)
                      
                      public:
                      	CListCtrlEx();
                      	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 );
                      
                      private:
                      	void SaveClmHdrInfo( LPCSTR sKey, int nClms, LPINT paiIndexes, LPINT paiWidths );
                      	BOOL ReadClmHdrInfo( LPCSTR sKey, int nClms, LPINT paiIndexes, LPINT paiWidths );
                      
                      protected:
                      	DECLARE_MESSAGE_MAP()
                      };
                      

Open in new window


CListCtrlEx source file
// ListCtrlEx.cpp : implementation file
                      //
                      #include "stdafx.h"
                      #include "ListClms.h"
                      #include "ListCtrlEx.h"
                      
                      IMPLEMENT_DYNAMIC(CListCtrlEx, CListCtrl)
                      
                      //----------------------------------- nothing special for the ctor or dtor
                      CListCtrlEx::CListCtrlEx(){}
                      CListCtrlEx::~CListCtrlEx(){}
                      
                      BEGIN_MESSAGE_MAP(CListCtrlEx, CListCtrl)
                      END_MESSAGE_MAP()
                      
                      /////////////////////////////////////////////////////////////////////////////
                      // Save and restore user prefs on column positions and widths
                      // to the registry key for this program
                      //
                      void CListCtrlEx::SaveClmInfo( LPCSTR szKey ) 
                      {
                      	HDITEM rHdr;
                      
                      	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 ) 
                      {
                      	HDITEM rHdr;
                      
                      	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*/ )
                      {
                      	LV_COLUMN  rLVC;
                      
                      	EraseList();  // delete all columns
                      
                      	while (parLCI->nId != -1 ) {
                      		rLVC.mask=     LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
                      		rLVC.cx=       parLCI->nWidth;     // in pixels
                      		rLVC.iSubItem= parLCI->nId;        // 
                      		rLVC.pszText=  parLCI->pszName;
                      		rLVC.fmt=      LVCFMT_LEFT;
                      
                      		int nRet= InsertColumn( parLCI->nClmNum, &rLVC );
                      		parLCI++;
                      	}
                      	if ( fSetFromSaved ) {
                      		UseSavedClmInfo( szKey );
                      	}
                      }
                      
                      void CListCtrlEx::EraseList()
                      {
                      	DeleteAllItems();
                      	while( DeleteColumn(0) )
                      		;
                      	UpdateWindow();
                      }
                      //--------------------------------------
                      // 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!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
3
7,755 Views
DanRollins
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.