Dragging columns to rearrange column sequence


Quite often the screen space available to the list view control is just not enough to display all the columns in the control. Also, very often, a user wants to rearrange the columns to his or her liking. The preferred way to deal with this is to allow dragging of the columns and thus allowing the user to rearrange the list.

There is no built in support for dragging columns although its likely to be introduced in the next release of the common controls. Heres how you can support it now.

Step 1: Create a custom header class

We create a custom header class derived from CHeaderCtrl to manage column dragging, give visual feedback to the user and finally to call a member function of the CListCtrl class to let it rearrange the list.

First the header file listing.

#if !defined(AFX_MYHEADERCTRL_H__CC3DDBF3_EF5E_11D0_82AD_9A0A48000000__INCLUDED_)
#define AFX_MYHEADERCTRL_H__CC3DDBF3_EF5E_11D0_82AD_9A0A48000000__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// MyHeaderCtrl.h : header file
//

/////////////////////////////////////////////////////////////////////////////
// CMyHeaderCtrl window

class CMyHeaderCtrl : public CHeaderCtrl
{
// Construction
public:
	CMyHeaderCtrl();
	CMyHeaderCtrl(CWnd* pWnd, void (CWnd::*fpDragCol)(int, int));

// Attributes
public:

// Operations
public:

// Overrides
	// ClassWizard generated virtual function overrides
	//{{AFX_VIRTUAL(CMyHeaderCtrl)
	//}}AFX_VIRTUAL

// Implementation
public:
	virtual ~CMyHeaderCtrl();
	void SetCallback(CWnd* pWnd, void (CWnd::*fpDragCol)(int, int));

protected:
	BOOL	m_bCheckForDrag;
	BOOL	m_bDragging;
	int	*m_pWidth;
	int	m_nDragCol;
	int	m_nDropPos;
	CRect	marker_rect;
	void	(CWnd::*m_fpDragCol)(int, int);
	CWnd	*m_pOwnerWnd;



	// Generated message map functions
protected:
	//{{AFX_MSG(CMyHeaderCtrl)
	afx_msg void OnMouseMove(UINT nFlags, CPoint point);
	afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
	afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
	//}}AFX_MSG

	DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////

//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_MYHEADERCTRL_H__CC3DDBF3_EF5E_11D0_82AD_9A0A48000000__INCLUDED_)
The CMyHeaderCtrl is derived from CHeaderCtrl. You will notice that the non default constructor is somewhat unusual. It takes a pointer to the CListCtrl or CListView derived class and also a pointer to a member function to call when the user has completed dragging the column. A member function SetCallback() is also defined. This function can be used when you want to use the default constructor for the CMyListCtrl class.

Heres a brief description of what the protected member variables are used for. The m_bCheckForDrag flag is set true by the WM_LBUTTONDOWN handler only when the user presses the left mouse button over a column header. Its used by the WM_MOUSEMOVE handler to decide whether it should check for a column drag situation. This is important since we want to drag the column only if the user initially pressed the mouse button over a column header.

The m_bDragging flag indicates that a column drag is in progress. The m_pWidth variable holds an array of column widths. This is used to determine the column that is the drop target. The array is dynamically allocated using the new operator. The m_nDragCol variable holds the column index of the column being dragged and m_nDropPos holds the column index of the new position. The marker_rect holds the enclosing rectangle for the marker used for visual feedback to the user. The marker is a triangle indicating the new position where the column will be dragged to. The marker_rect is used to erase the previous marker when the marker position changes.

The m_fpDragCol variable holds a pointer to the CListCtrl or CListView member function that gets called when the user finishes the drag operation. The m_pOwnerWnd variable holds the object for which the m_fpDragCol member function is called. Normally, this would be the parent window.

Now, onto the implementation file listing.

// MyHeaderCtrl.cpp : implementation file
//

#include "stdafx.h"
#include "MyHeaderCtrl.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// CMyHeaderCtrl

CMyHeaderCtrl::CMyHeaderCtrl() 
	: marker_rect(0,0,0,0)
{
	m_pWidth = NULL;
	m_bDragging = FALSE;
	m_bCheckForDrag = FALSE;
	m_fpDragCol = NULL;
	m_pOwnerWnd = NULL;
}

CMyHeaderCtrl::CMyHeaderCtrl(CWnd *pWnd, void (CWnd::*fpDragCol)(int, int)) 
	: marker_rect(0,0,0,0)
{
	m_pWidth = NULL;
	m_bDragging = FALSE;
	m_bCheckForDrag = FALSE;
	m_fpDragCol = fpDragCol;
	m_pOwnerWnd = pWnd;
}

CMyHeaderCtrl::~CMyHeaderCtrl()
{
}


BEGIN_MESSAGE_MAP(CMyHeaderCtrl, CHeaderCtrl)
	//{{AFX_MSG_MAP(CMyHeaderCtrl)
	ON_WM_MOUSEMOVE()
	ON_WM_LBUTTONUP()
	ON_WM_LBUTTONDOWN()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CMyHeaderCtrl message handlers

void CMyHeaderCtrl::OnMouseMove(UINT nFlags, CPoint point) 
{
	if( (MK_LBUTTON & nFlags) == 0)
	{
		// The left mouse button is not pressed - so reset flags
		m_bCheckForDrag = FALSE;
		m_bDragging = FALSE;
	}
	else if( m_bDragging )
	{
		// Get column number that falls under the mouse
		int i=0, cx = 0;
		if( point.x > 0 )
			for( i = 0; i < GetItemCount(); i++ )
			{
				if( point.x >= cx && point.x < cx + m_pWidth[i] )
					break;
				cx += m_pWidth[i];
			}

		if( i != m_nDropPos )
		{
			m_nDropPos = i;

			CRect rect;
			GetWindowRect( &rect );

			// Invalidate area occupied by previous marker
			InvalidateRect( &marker_rect );

			// Draw a new marker
			CClientDC dc(this);
			POINT pts[3];
			pts[0].x = cx; pts[1].x = cx -3; pts[2].x = cx +3;
			pts[0].y = rect.Height(); pts[1].y = pts[2].y = rect.Height() -7; 
			dc.Polygon( pts, 3 );

			// save marker information
			marker_rect.left = cx - 4;
			marker_rect.top = rect.Height() -8;
			marker_rect.right = cx + 4;
			marker_rect.bottom = rect.Height();
		}
		return;
	}
	else if( m_bCheckForDrag )
	{
		// The mouse button was pressed over a column header
		// and now the mouse has moved - so start drag
		m_bCheckForDrag = FALSE;

		m_bDragging = TRUE;
		m_nDropPos = m_nDragCol;

		SetCapture();

		// Store information for later use
		int iCount = GetItemCount();
		HD_ITEM hd_item;
		m_pWidth = new int[iCount];
		for( int i = 0; i < iCount; i++ )
		{
			hd_item.mask = HDI_WIDTH;
			GetItem( i, &hd_item );
			m_pWidth[i] = hd_item.cxy;

		}
		return;
	}
	
	CHeaderCtrl::OnMouseMove(nFlags, point);
}

void CMyHeaderCtrl::OnLButtonUp(UINT nFlags, CPoint point) 
{
	ASSERT( m_pOwnerWnd != NULL && m_fpDragCol != NULL );

	if( m_bDragging )
	{
		m_bDragging = FALSE;
		delete[] m_pWidth;
		ReleaseCapture();
		Invalidate();

		// Call the callback function.
		if( m_nDragCol != m_nDropPos && m_nDragCol != m_nDropPos -1 )

			(m_pOwnerWnd->*m_fpDragCol)( m_nDragCol, m_nDropPos );
	}
	CHeaderCtrl::OnLButtonUp(nFlags, point);
}


void CMyHeaderCtrl::SetCallback( CWnd* pWnd, void (CWnd::*fpDragCol)(int, int) )
{
	m_fpDragCol = fpDragCol;
	m_pOwnerWnd = pWnd;
}

void CMyHeaderCtrl::OnLButtonDown(UINT nFlags, CPoint point) 
{
	// Determine if mouse was pressed over a column header
	HD_HITTESTINFO hd_hittestinfo;
	hd_hittestinfo.pt = point;
	SendMessage(HDM_HITTEST, 0, (LPARAM)(&hd_hittestinfo));
	if( hd_hittestinfo.flags == HHT_ONHEADER )
	{
		m_nDragCol = hd_hittestinfo.iItem;
		m_bCheckForDrag = TRUE;
	}
	
	CHeaderCtrl::OnLButtonDown(nFlags, point);
}
The implementation of the CMyHeaderCtrl is fairly straight-forward. It essentially has handlers for three windows messages - WM_MOUSEMOVE, WM_LBUTTONDOWN and WM_LBUTTONUP.

OnLButtonDown() sets the value for m_nDragCol and sets the m_bCheckForDrag flag if the user pressed the mouse button over a column header.

OnMouseMove() is where the visual feedback is given. It first checks whether the left mouse button is down and resets the m_bCheckForDrag and the m_bDragging flags. If a drag is in process, the m_nDropPos value is set and the marker is drawn in the header. Finally, if the first two conditions fail, it checks whether a drag should be initiated.

OnLButtonUp() ends the drag process if there was a drag underway and calls the callback function with the drag column and the drop position as arguments.
 

Step 2: Add a CMyHeaderCtrl member variable in the CListCtrl derived class

Add a CMyHeaderCtrl member variable in the CListCtrl derived class. If you are using a CListView derivative, add the member variable to that class.
	CMyHeaderCtrl   m_headerctrl;

Step 3: Initialize the CMyHeaderCtrl object

Add the following statement in the constructor of the CListCtrl derived class.
	m_headerctrl.SetCallback( this, (void (CWnd::*)(int, int))DragColumn );
DragColumn is the callback function we will define in the next step.

Step 4: Define a callback function for rearranging columns

The CMyHeaderCtrl object needs a function pointer that it uses to call the function when the user has completed the drag operation. It is this callback function that is actually responsible for rearranging the columns. We used the name DragColumn in the previous step when initializing the CMyHeaderCtrl object.
void CMyListCtrl::DragColumn(int source, int dest)
{
	TCHAR sColText[160];

	// Insert a column at dest
	LV_COLUMN       lv_col;
	lv_col.mask = LVCF_FMT | LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
	lv_col.pszText = sColText;
	lv_col.cchTextMax = 159;
	GetColumn( source, &lv_col );
	lv_col.iSubItem = dest;
	InsertColumn( dest, &lv_col );

	// Adjust source col number since it might have changed 
	// because a new column was inserted
	if( source > dest ) 
		source++;

	// Moving a col to position 0 is a special case
	if( dest == 0 )
		for( int i = GetItemCount()-1; i > -1 ; i-- )
			SetItemText(i, 1, GetItemText( i, 0) );

	
	// Copy sub item from source to dest
	for( int i = GetItemCount()-1; i > -1 ; i-- )
		SetItemText(i, dest, GetItemText( i, source ) );

	// Delete the source column, but not if it is the first
	if( source != 0 )
		DeleteColumn( source );
	else
	{
		// If source col is 0, then copy col# 1 to col#0
		// and then delete col# 1
		GetColumn( 1, &lv_col );
		lv_col.iSubItem = 0;
		SetColumn( 0, &lv_col );
		for( int i = GetItemCount()-1; i > -1 ; i-- )
			SetItemText(i, 0, GetItemText( i, 1) );
		DeleteColumn( 1 );
	}

	Invalidate();
}
The general approach we take in this function is that we insert a column at the right place, copy all sub-items from the source column and then delete the source column. The list view control doles out special treatment to the first column and therefore we also have to. Here are the special behaviour you have to be aware of when inserting or deleting a column. When you try to insert a column at position zero and the control already has atleast one column, then the new column is actually inserted as the second column. When you delete the first column then the result is that the column headers are shifted left by one and the last column is deleted. The DragColumn() function handles these two situations.
 

Step 5: Subclass the header control

Finally we need to subclass the header control. A good place to do this is in the PreSubclassWindow() override of our list view control class. If you are using a CListView derived class, you can place the sub-classing code in OnInitialUpdate(). 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 listview control before trying the subclass the header 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 listview control is first taken to the report view mode.
void CMyListCtrl::PreSubclassWindow() 
{
	CListCtrl::PreSubclassWindow();

	// Add initialization code
	m_headerctrl.SubclassWindow( ::GetDlgItem(m_hWnd,0) );
}
 



Comments

  • Save order to Registry and automatic scroll

    Posted by Legacy on 04/22/2002 12:00am

    Originally posted by: SnapRick

    How can I save this to the registry on the destroy of the column and fix the drag&drop when the new position is outside the current display listctrl window?

    Any assistance would be greatly appreciated.

    Reply
  • does not auto scroll when dragging a column header

    Posted by Legacy on 03/28/2000 12:00am

    Originally posted by: Tianming Zhang

    I am looking for a fix to this problem. Any suggestion is more than welcome!

    When I drag&drop a column header to re-arrange col order, if the new position is outside the current display listctrl window, it does not automatically scroll.

    Reply
  • Enable drag and drop columns using a macro.

    Posted by Legacy on 05/25/1999 12:00am

    Originally posted by: Nick Kolev

    I tried T.Jerry's suggestion. It actually did not work for me, but it was a good point to start from. :-)
    I had a list control in a dialog box. And in OnInitDialog() I added call to ListView_SetExtendedListViewStyle macro.
    This macro sends LVM_SETEXTENDEDLISTVIEWSTYLE message and does the magic. :-)
    This macro has 2 parameters - HWND and new extended style you want to add. Make your call to look like this :
    ListView_SetExtendedListViewStyle( MyListCotrl.m_hWnd, LVS_EX_HEADERDRAGDROP )


    Reply
  • Disable drag and drop

    Posted by Legacy on 05/25/1999 12:00am

    Originally posted by: Nick Kolev

    If you want to disable drag and drop mode for columns, modify the flag which sets it.
    Your call will be something like this :
    ListView_SetExtendedListViewStyle( pMyListCtrl->m_hWnd, LVS_EX_HEADERDRAGDROP & 0x11111101 );
    I agree it is bit of a hack but the other way is to write tons of code...

    Reply
  • RE: How to implement Find?

    Posted by Legacy on 04/28/1999 12:00am

    Originally posted by: LJP

    Search for Article ID: Q175513 titled- FILE: Invoke Find, View Source, Options dialogs for WebBrowser. I haven't done this one, yet.

    Reply
  • Use built-in drag&drop (version 4.70)

    Posted by Legacy on 02/13/1999 12:00am

    Originally posted by: T. Jerry


    The new Common Controls version (4.70) enables drag-and-drop reordering of columns in a list view control.

    This style is only available to list view controls that use the LVS_REPORT style.

    The only thing to do , call :

    CListCtrl::SetExtendedStyle(LVS_EX_HEADERDRAGDROP);

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • Relying on outside companies to manage your network and server environments for your business and applications to meet the needs and demands of your users can be stressful. This is especially true as many Managed Hosting organizations fail to meet their service level agreements. Read this Forrester total economic impact report and learn what makes INetU different and how they exceed their customers' managed hosting expectations.

Most Popular Programming Stories

More for Developers

RSS Feeds