Dragging Items to Rearrange Rows


This article was contributed by Wayne Berthin.

This article describes how to impletment drag and drop rearrangement of rows in a list control with the report style. The drag operation starts when the user begins a left drag on any item label (in its left-most column). During the drag a ghost image of the item is diplayed. When dropped, the item is inserted into the new position in the list. The user can thus quickly and easily rearange the order of row items in the List Control. The method is easily adapted to moving rows between different List Controls or into other types of objects as required.

I have taken this code from my Application where I have a CListCtrl derived class contained in a CView derived class. It should be addaptable to other arrangements with minor modifications.

In the header file for CMyView.h I have defined the following members:
protected:
	CImageList* m_pDragImage;
	BOOL m_bDragging;
	int m_nDragIndex, m_nDropIndex;
	CWnd* m_pDropWnd;
	CPoint m_ptDropPoint;

I also have an object of my derived list control class as a member of CMyView named m_ListControl. It is created using CListCtr::Create and attached to the view, which is a whole other subject.

CMyView needs the following member functions to implement drag and drop:

protected:
	void DropItemOnList();
	
	afx_msg void OnMouseMove(UINT nFlags, CPoint point);
	afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
	afx_msg void OnBeginDrag(NMHDR* pNMHDR, LRESULT* pResult);
	
	DECLARE_MESSAGE_MAP()

OnMouseMove and OnLButtonUp can be added with the class Wizard but OnBeginDrag has to be inserted mannually.

Then in the CMyView.cpp file Message Map we require the following entries:

BEGIN_MESSAGE_MAP(CSearchResultView, CListCtrlView)

...

ON_WM_MOUSEMOVE()
ON_WM_LBUTTONUP()
ON_NOTIFY(LVN_BEGINDRAG, IDI_SEARCH_RESULT_LIST, OnBeginDrag)

END_MESSAGE_MAP()

Initialize the member variables that control the operation in the Class constructor,

CMyView::CMyView()
{
	m_pItemList = NULL;
	m_pItemList = new CItemList(this);
	m_bDragging = FALSE;
}
and clean up in the class destructor.
CMyView::~CMyView()
{
	delete m_pItemList;
}

The whole drag and drop operation is handled by the four member functions, beginning with OnBeginDrag. This functions creates a drag image and initializes the drag operation using the CListCtrl::CreateDragImage, CImageList::BeginDrag and CImageList::DragEnter.

CreateDragImage takes the item label and image item associated with the row and creates a lightened drag image so the user can see the item moving as he drags it. The item itself is also left in its fixed state in the list until a valid drop occurs. Note if you are using an OwnerDraw List Control (as I was) you will have to temporarily turn off that feature or CreateDragImage will not give you a proper image if it is left on. Insert the line:

m_ListControl.ModifyStyle(LVS_OWNERDRAWFIXED, 0);
immediately before the call to CreateDragImage to disable owner draw, and the line
m_ListControl.ModifyStyle(0, LVS_OWNERDRAWFIXED);
right after the call to CreateDragImage to reinstate owner draw after the drag image is created.

BeginDrag initializes the drag image and enables it to be moved by subsequent calls to CImageList::DragMove. DragEnter locks updates to the Window during the drag operation. The mouse is captured for the duration of the drag with SetCapture which preserves the integrity of the operation even if the user drags the item outside of the current View. A flag (m_bDragging)is set so subsequent mouse messages can be interpretted in the context of the drag operation underway.

void CMyView::OnBeginDrag(NMHDR* pnmhdr, LRESULT* pResult)
{ 
	//RECORD THE INDEX OF THE ITEM BEIGN DRAGGED AS m_nDragIndex
	m_nDragIndex = ((NM_LISTVIEW *)pnmhdr)->iItem;
	
	//CREATE A DRAG IMAGE FROM THE CENTER POINT OF THE ITEM IMAGE
	POINT pt;
	pt.x = 8;
	pt.y = 8;
	m_pDragImage = m_ListControl.CreateDragImage(m_nDragIndex, &pt);
	m_pDragImage->BeginDrag(0, CPoint (8, 8));
	m_pDragImage->DragEnter(
		GetDesktopWindow(), ((NM_LISTVIEW *)pnmhdr)->ptAction);
	
	//SET THE FLAGS INDICATING A DRAG IN PROGRESS
	m_bDragging = TRUE;
	m_hDropItem = NULL;
	m_nDropIndex = -1;
	m_pDropWnd = &m_ListControl;
	
	//CAPTURE ALL MOUSE MESSAGES IN CASE THE USER DRAGS OUTSIDE OF THE VIEW
	SetCapture();
}

During the drag operation the mouse movements are monitored by OnMouseMove to update the current location of the drag image and the drop point. Drop Highlighting of the drop target could be added here if desired.

void CMyView::OnMouseMove(UINT nFlags, CPoint point) 
{
	if( m_bDragging )
	{
		m_ptDropPoint = point;
		ClientToScreen(&m_ptDropPoint);
		
		//MOVE THE DRAG IMAGE
		m_pDragImage->DragMove(m_ptDropPoint);
		
		//TEMPORARILY UNLOCK WINDOW UPDATES
		m_pDragImage->DragShowNolock(FALSE);
		
		//CONVERT THE DROP POINT TO CLIENT CO-ORDIANTES
		m_ pDropWnd = WindowFromPoint(m_ptDropPoint);
		m_pDropWnd->ScreenToClient(&m_ptDropPoint);
		
		//LOCK WINDOW UPDATES
		m_pDragImage->DragShowNolock(TRUE);
	}
	
	CView::OnMouseMove(nFlags, point);
}

The WM_LBUTTONUP will signal that a drop has occurred. It is necessage to verify the type of Window the item has been dropped upon. The function CObject::IsKindOf can be used for this.

void CSearchResultView::OnLButtonUp(UINT nFlags, CPoint point) 
{
	if( m_bDragging )
	{
		//RELEASE THE MOUSE CAPTURE AND END THE DRAGGING
		::ReleaseCapture();
		m_bDragging = FALSE;
		m_pDragImage->DragLeave(GetDesktopWindow());
		m_pDragImage->EndDrag();
		
		//GET THE WINDOW UNDER THE DROP POINT
		CPoint pt(point);
		ClientToScreen(&pt);
		m_pDropWnd = WindowFromPoint(pt);
		
		//DROP THE ITEM ON THE LIST
		if( pDropWnd->IsKindOf(RUNTIME_CLASS(CListCtrl)) )
			DropItemOnList();
	}
	CView::OnLButtonUp(nFlags, point);
}
The final step is to drop the item on the list. This means Insert the dragged item at the drop point and delete it from its previous location. Here I offset the drop point 10 pixels so items dropped between other items end up between them. I created a CMyView::HitTest based on the the method outlined in the Code Guru article "Detecting column index of the item clicked" so that a click anywhere on the row would suffice. See that article and its discussions of the the short-comings of the CListCtrl::HitTest.
void CSearchResultView::DropItemOnList()
{
	//GET THE DROP INDEX
	m_ptDropPoint.y += 10;
	m_nDropIndex = HitTest(m_ptDropPoint);
	
	//GET INFORMATION ON THE DRAGGED ITEM BY SETTING AN LV_ITEM STRUCTURE
	//AND THEN CALLING GetItem TO FILL IT IN
	char szLabel[256];
	LV_ITEM lvi;
	ZeroMemory(&lvi, sizeof(LV_ITEM));
	lvi.mask = LVIF_TEXT | LVIF_IMAGE | LVIF_STATE | LVIF_PARAM;
	lvi.stateMask = LVIS_DROPHILITED | LVIS_FOCUSED | LVIS_SELECTED;
	lvi.pszText = szLabel;
	lvi.iItem = m_nDragIndex;
	lvi.cchTextMax = 255;
	m_ListControl.GetItem(&lvi);
	
	//INSERT THE DROPPED ITEM
	if(m_nDropIndex < 0) m_nDropIndex = m_ListControl.GetItemCount();
	lvi.iItem = m_nDropIndex;
	m_ListControl.InsertItem(&lvi);
	
	//FILL IN ALL OF THE COLUMNS
	CHeaderCtrl* pHeader = (CHeaderCtrl*)m_ListControl.GetDlgItem(0);
	int nColumnCount = pHeader->GetItemCount();
	lvi.mask = LVIF_TEXT;
	lvi.iItem = m_nDropIndex;
	//INDEX OF DRAGGED ITEM WILL CHANGE IF ITEM IS DROPPED ABOVE ITSELF
	if(m_nDragIndex > m_nDropIndex) m_nDragIndex++;
	for(int col=1; col < nColumnCount; col++)
	{
		strcpy(lvi.pszText, (LPCTSTR)(m_ListControl.GetItemText(m_nDragIndex,
			col)));
		lvi.iSubItem = col;
		m_ListControl.SetItem(&lvi);
	}
	
	//DELETE THE ITEM THAT WAS DRAGGED FROM ITS ORIGINAL LOCATION
	m_ListControl.DeleteItem(m_nDragIndex);
}



Comments

  • pen with name bfgi

    Posted by Louissxxg on 03/11/2013 07:42am

    vetement louis vuitton pas cher lunette louis vuitton pas cher tencxz louis vuitton pas cher homme sac louis vuitton rphmek

    Reply
  • I would suggest to copy the item data of the dragged item

    Posted by toro on 12/20/2004 04:11am

    In a CListCtrl an item data value can be assigned to each
    item using either SetItemData or SetItemDataPointer. In
    DropItemOnList we should copy the item data of the item
    that is dropped to a new position, so that this information
    does not get lost.
    
    To do so, insert the following code
    
         // COPY ITEM DATA POINTER
         SetItemData(m_nDropIndex, GetItemData(m_nDragIndex));
    
    right before this line:
    	
         //DELETE THE ITEM THAT WAS DRAGGED FROM ITS ORIGINAL LOCATION
         DeleteItem(m_nDragIndex);
    
    (in DropItemOnList)

    Reply
  • Help me!!!

    Posted by Legacy on 03/14/2002 12:00am

    Originally posted by: Sivaram

    I need to drag files from the List control to NT Explorer.Any idea..........?

    Reply
  • It does not work with WS_EX_ACCEPTFILES style of ListCtrl

    Posted by Legacy on 01/30/2002 12:00am

    Originally posted by: Y.J.Kim

    Thanks to your nice joob.
    Your article help me very much.
    However, I want to add more functions to my ListCtrl such as Drag&Drop from Explorer.
    So I create my ListCtrl with WS_EX_ACCEPTFILES style (and LVS_REPORT style).
    Just adding WS_EX_ACCEPTFILES style when creating ListCtrl makes your code not work.
    Please help me what's the problem..

    Reply
  • Sample project !!!!

    Posted by Legacy on 12/07/2001 12:00am

    Originally posted by: Shripad Kulkarni

    A sample project would be really helpful !!

    Reply
  • works well with a little bit of fixing

    Posted by Legacy on 12/05/2001 12:00am

    Originally posted by: Chris

    this works right... you just need to twee it. Good job.

    Reply
  • Ghosting data

    Posted by Legacy on 01/25/2001 12:00am

    Originally posted by: N Neira

    Attn:

    To whom it may concern,
    I need some code to hide text from one screen to the next. Can anyone give a hand with my problem?

    The first screen sends the data to a second screen
    Then on the second screen
    The user is to choose the patient data
    Which in return
    Go's to the first screen to be edited by the user


    (Ghosting) the data is what they call it.
    But its not working for me.

    Thanks for ur time,
    vbrules

    Reply
  • thanks - now let's scroll the list

    Posted by Legacy on 01/02/2001 12:00am

    Originally posted by: edlogic

    i tried putting the code to scroll in OnMouseMove and it works 
    
    but scrolling stops unless you keep moving the mouse .
    so i created a timer that scrolls as long as you stay over the
    first or last item in the list .


    void CMyClass::OnMouseMove(UINT nFlags, CPoint point)
    {
    if( m_bDragging )
    {
    m_ptDropPoint = point;
    ClientToScreen(&m_ptDropPoint);

    m_pDragImage->DragMove(m_ptDropPoint);
    m_pDropWnd = WindowFromPoint(m_ptDropPoint);
    m_pDropWnd->ScreenToClient(&m_ptDropPoint);

    int iOverItem = m_mylist.HitTest(m_ptDropPoint);
    int iTopItem = m_mylist.GetTopIndex();
    int iBottomItem = iTopItem + m_mylist.GetCountPerPage() - 1;

    if (iOverItem == iTopItem && iTopItem != 0) //top of list
    SetTimer(1, 33, NULL);
    else
    KillTimer(1);
    //bottom of list
    if (iOverItem == iBottomItem && iBottomItem != (m_mylist.GetItemCount() - 1))
    SetTimer(3, 33, NULL);
    else
    KillTimer(3);
    }

    CDialog::OnMouseMove(nFlags, point);
    }

    OnTimer has some functionality that was previously in OnMouseMove .


    void CMyClass::OnTimer(UINT nIDEvent)
    {
    int iTopItem = m_mylist.GetTopIndex();
    int iBottomItem = iTopItem + m_mylist.GetCountPerPage() - 1;

    if( m_bDragging )
    {
    m_pDragImage->DragShowNolock(FALSE);

    if (nIDEvent == 1)
    {
    m_mylist.EnsureVisible(iTopItem - 1, false);
    ::UpdateWindow(m_mylist.m_hWnd);
    }
    else if (nIDEvent == 3)
    {
    m_mylist.EnsureVisible(iBottomItem + 1, false);
    ::UpdateWindow(m_mylist.m_hWnd);
    }

    m_pDragImage->DragShowNolock(TRUE);
    }

    CDialog::OnTimer(nIDEvent);

    also kill the timers in CMyClass::OnLButttonUp() - when the drag is complete .

    Reply
  • Fiddling needed, but working GREAT

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

    Originally posted by: Crash

    I just wanted to thank you for this code. With a little fiddling I finally got it to work. Anyone who is trying to use this, read the post concerning typo's, he's correct.

    Thanks again :-)

    -Crash

    Reply
  • Lost !

    Posted by Legacy on 06/29/1999 12:00am

    Originally posted by: Hans Wedemeyer

    Looks like something I would use, but I'm lost in the description, where did CSearchResultView suddenly come from ?
    and where did the LIST control ID come from ?

    An example project would be great !

    Thanks

    Hans Wedemeyer

    Reply
  • Loading, Please Wait ...

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 …

  • With the average hard drive now averaging one terabyte in size, the fallout from the explosion of user-created data has become an overwhelming volume of potential evidence that law-enforcement and corporate investigators spend countless hours examining. Join Us and SANS' Rob Lee for our 45-minute webinar, A Triage and Collection Strategy for Time-Sensitive Investigations, will demonstrate how to: Identify the folders and files that often contain key insights Reduce the time spent sifting through content by …

Most Popular Programming Stories

More for Developers

RSS Feeds